From bdb01d59fa7aab4339d136b7b780b671073108fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Scho=CC=88nthal?= Date: Thu, 13 Mar 2014 23:23:45 +0100 Subject: [PATCH] fixed everything to be psr2 compatible (php-cs-fixer) --- apps/advanced/backend/assets/AppAsset.php | 16 +- apps/advanced/backend/config/main.php | 56 +- apps/advanced/backend/config/params.php | 2 +- .../backend/controllers/SiteController.php | 125 +- apps/advanced/backend/tests/_config.php | 16 +- .../backend/tests/_helpers/CodeHelper.php | 2 +- .../backend/tests/_helpers/TestHelper.php | 2 +- .../backend/tests/_helpers/WebHelper.php | 2 +- .../backend/tests/acceptance.suite.yml | 2 +- .../backend/tests/acceptance/_config.php | 24 +- .../backend/tests/acceptance/_console.php | 22 +- .../backend/tests/functional/_config.php | 24 +- .../backend/tests/functional/_console.php | 22 +- .../backend/tests/unit/DbTestCase.php | 2 +- apps/advanced/backend/tests/unit/TestCase.php | 2 +- apps/advanced/backend/tests/unit/_config.php | 24 +- apps/advanced/backend/tests/unit/_console.php | 22 +- apps/advanced/backend/views/layouts/main.php | 92 +- apps/advanced/backend/views/site/error.php | 20 +- apps/advanced/backend/views/site/index.php | 66 +- apps/advanced/backend/views/site/login.php | 28 +- apps/advanced/common/config/main.php | 14 +- apps/advanced/common/config/params.php | 6 +- apps/advanced/common/mail/layouts/html.php | 14 +- .../common/mail/passwordResetToken.php | 2 +- apps/advanced/common/models/LoginForm.php | 113 +- apps/advanced/common/models/User.php | 364 +- apps/advanced/common/tests/_config.php | 16 +- .../common/tests/_helpers/CodeHelper.php | 2 +- .../common/tests/_helpers/FixtureHelper.php | 74 +- .../common/tests/_helpers/TestHelper.php | 2 +- .../common/tests/_helpers/WebHelper.php | 2 +- .../common/tests/_pages/LoginPage.php | 22 +- .../common/tests/fixtures/UserFixture.php | 2 +- .../common/tests/fixtures/data/init_login.php | 20 +- .../tests/templates/fixtures/tbl_user.php | 49 +- .../advanced/common/tests/unit/DbTestCase.php | 2 +- apps/advanced/common/tests/unit/TestCase.php | 2 +- apps/advanced/common/tests/unit/_config.php | 24 +- apps/advanced/common/tests/unit/_console.php | 22 +- .../tests/unit/models/LoginFormTest.php | 113 +- apps/advanced/console/config/main.php | 38 +- apps/advanced/console/config/params.php | 2 +- .../migrations/m130524_201442_init.php | 46 +- apps/advanced/console/tests/_config.php | 16 +- .../console/tests/_helpers/CodeHelper.php | 2 +- .../console/tests/_helpers/TestHelper.php | 2 +- .../console/tests/_helpers/WebHelper.php | 2 +- .../console/tests/unit/DbTestCase.php | 2 +- apps/advanced/console/tests/unit/TestCase.php | 2 +- apps/advanced/console/tests/unit/_config.php | 20 +- apps/advanced/console/tests/unit/_console.php | 22 +- .../dev/backend/config/main-local.php | 8 +- .../dev/backend/web/index-test.php | 2 +- .../environments/dev/backend/web/index.php | 8 +- .../dev/common/config/main-local.php | 28 +- .../dev/frontend/config/main-local.php | 8 +- .../dev/frontend/web/index-test.php | 2 +- .../environments/dev/frontend/web/index.php | 8 +- apps/advanced/environments/index.php | 36 +- .../environments/prod/backend/web/index.php | 8 +- .../prod/common/config/main-local.php | 26 +- .../environments/prod/frontend/web/index.php | 8 +- apps/advanced/frontend/assets/AppAsset.php | 22 +- apps/advanced/frontend/config/main.php | 52 +- apps/advanced/frontend/config/params.php | 2 +- .../frontend/controllers/SiteController.php | 298 +- apps/advanced/frontend/models/ContactForm.php | 86 +- .../models/PasswordResetRequestForm.php | 82 +- .../frontend/models/ResetPasswordForm.php | 91 +- apps/advanced/frontend/models/SignupForm.php | 67 +- apps/advanced/frontend/tests/_config.php | 16 +- .../frontend/tests/_helpers/CodeHelper.php | 2 +- .../frontend/tests/_helpers/TestHelper.php | 2 +- .../frontend/tests/_helpers/WebHelper.php | 2 +- .../frontend/tests/_pages/AboutPage.php | 2 +- .../frontend/tests/_pages/ContactPage.php | 24 +- .../frontend/tests/_pages/SignupPage.php | 24 +- .../frontend/tests/acceptance.suite.yml | 2 +- .../frontend/tests/acceptance/ContactCept.php | 20 +- .../frontend/tests/acceptance/SignupCest.php | 120 +- .../frontend/tests/acceptance/_config.php | 24 +- .../frontend/tests/acceptance/_console.php | 22 +- .../frontend/tests/functional/ContactCept.php | 20 +- .../frontend/tests/functional/SignupCest.php | 158 +- .../frontend/tests/functional/_config.php | 24 +- .../frontend/tests/functional/_console.php | 22 +- .../frontend/tests/unit/DbTestCase.php | 2 +- .../advanced/frontend/tests/unit/TestCase.php | 2 +- apps/advanced/frontend/tests/unit/_config.php | 24 +- .../advanced/frontend/tests/unit/_console.php | 22 +- .../tests/unit/fixtures/data/tbl_user.php | 38 +- .../tests/unit/models/ContactFormTest.php | 94 +- .../models/PasswordResetRequestFormTest.php | 114 +- .../unit/models/ResetPasswordFormTest.php | 42 +- .../tests/unit/models/SignupFormTest.php | 70 +- apps/advanced/frontend/views/layouts/main.php | 102 +- apps/advanced/frontend/views/site/about.php | 6 +- apps/advanced/frontend/views/site/contact.php | 40 +- apps/advanced/frontend/views/site/error.php | 20 +- apps/advanced/frontend/views/site/index.php | 66 +- apps/advanced/frontend/views/site/login.php | 34 +- .../views/site/requestPasswordResetToken.php | 24 +- .../frontend/views/site/resetPassword.php | 24 +- apps/advanced/frontend/views/site/signup.php | 28 +- apps/advanced/frontend/widgets/Alert.php | 76 +- apps/advanced/requirements.php | 166 +- apps/basic/assets/AppAsset.php | 22 +- apps/basic/commands/HelloController.php | 16 +- apps/basic/config/console.php | 40 +- apps/basic/config/db.php | 10 +- apps/basic/config/params.php | 2 +- apps/basic/config/web.php | 68 +- apps/basic/controllers/SiteController.php | 148 +- apps/basic/mail/layouts/html.php | 14 +- apps/basic/models/ContactForm.php | 95 +- apps/basic/models/LoginForm.php | 111 +- apps/basic/models/User.php | 174 +- apps/basic/requirements.php | 166 +- apps/basic/tests/_config.php | 16 +- apps/basic/tests/_helpers/CodeHelper.php | 2 +- apps/basic/tests/_helpers/TestHelper.php | 2 +- apps/basic/tests/_helpers/WebHelper.php | 2 +- apps/basic/tests/_pages/AboutPage.php | 2 +- apps/basic/tests/_pages/ContactPage.php | 24 +- apps/basic/tests/_pages/LoginPage.php | 22 +- apps/basic/tests/acceptance.suite.yml | 2 +- apps/basic/tests/acceptance/ContactCept.php | 22 +- apps/basic/tests/acceptance/LoginCept.php | 2 +- apps/basic/tests/acceptance/_config.php | 18 +- apps/basic/tests/acceptance/_console.php | 18 +- apps/basic/tests/functional/ContactCept.php | 20 +- apps/basic/tests/functional/_config.php | 18 +- apps/basic/tests/functional/_console.php | 18 +- apps/basic/tests/unit/_config.php | 18 +- apps/basic/tests/unit/_console.php | 18 +- .../tests/unit/models/ContactFormTest.php | 96 +- .../basic/tests/unit/models/LoginFormTest.php | 89 +- apps/basic/tests/unit/models/UserTest.php | 14 +- apps/basic/views/layouts/main.php | 82 +- apps/basic/views/site/about.php | 10 +- apps/basic/views/site/contact.php | 86 +- apps/basic/views/site/error.php | 20 +- apps/basic/views/site/index.php | 66 +- apps/basic/views/site/login.php | 50 +- apps/basic/web/index-test.php | 2 +- apps/benchmark/index.php | 14 +- .../protected/controllers/SiteController.php | 10 +- extensions/apidoc/commands/ApiController.php | 276 +- .../apidoc/commands/GuideController.php | 187 +- .../apidoc/components/BaseController.php | 213 +- extensions/apidoc/helpers/ApiMarkdown.php | 451 ++- extensions/apidoc/helpers/PrettyPrinter.php | 37 +- extensions/apidoc/models/BaseDoc.php | 129 +- extensions/apidoc/models/ClassDoc.php | 171 +- extensions/apidoc/models/ConstDoc.php | 30 +- extensions/apidoc/models/Context.php | 575 +-- extensions/apidoc/models/EventDoc.php | 57 +- extensions/apidoc/models/FunctionDoc.php | 106 +- extensions/apidoc/models/InterfaceDoc.php | 58 +- extensions/apidoc/models/MethodDoc.php | 44 +- extensions/apidoc/models/ParamDoc.php | 70 +- extensions/apidoc/models/PropertyDoc.php | 110 +- extensions/apidoc/models/TraitDoc.php | 38 +- extensions/apidoc/models/TypeDoc.php | 332 +- extensions/apidoc/renderers/ApiRenderer.php | 25 +- extensions/apidoc/renderers/BaseRenderer.php | 335 +- extensions/apidoc/renderers/GuideRenderer.php | 26 +- .../templates/bootstrap/ApiRenderer.php | 155 +- .../templates/bootstrap/GuideRenderer.php | 49 +- .../templates/bootstrap/RendererTrait.php | 156 +- .../templates/bootstrap/SideNavWidget.php | 233 +- .../bootstrap/assets/AssetBundle.php | 24 +- .../templates/bootstrap/layouts/api.php | 121 +- .../templates/bootstrap/layouts/guide.php | 52 +- .../templates/bootstrap/layouts/main.php | 99 +- .../templates/bootstrap/views/index.php | 28 +- .../apidoc/templates/html/ApiRenderer.php | 514 +-- .../apidoc/templates/html/GuideRenderer.php | 255 +- .../templates/html/views/constSummary.php | 46 +- .../templates/html/views/eventDetails.php | 55 +- .../templates/html/views/eventSummary.php | 54 +- .../templates/html/views/methodDetails.php | 102 +- .../templates/html/views/methodSummary.php | 24 +- .../templates/html/views/propertyDetails.php | 61 +- .../templates/html/views/propertySummary.php | 28 +- .../apidoc/templates/html/views/seeAlso.php | 36 +- .../apidoc/templates/html/views/type.php | 122 +- .../apidoc/templates/online/ApiRenderer.php | 80 +- .../apidoc/templates/online/views/index.php | 26 +- extensions/authclient/AuthAction.php | 617 +-- extensions/authclient/BaseClient.php | 413 +- extensions/authclient/BaseOAuth.php | 973 ++--- extensions/authclient/ClientInterface.php | 78 +- extensions/authclient/Collection.php | 117 +- extensions/authclient/OAuth1.php | 618 +-- extensions/authclient/OAuth2.php | 305 +- extensions/authclient/OAuthToken.php | 358 +- extensions/authclient/OpenId.php | 1785 ++++---- extensions/authclient/clients/Facebook.php | 74 +- extensions/authclient/clients/GitHub.php | 92 +- extensions/authclient/clients/GoogleOAuth.php | 92 +- .../authclient/clients/GoogleOpenId.php | 98 +- extensions/authclient/clients/LinkedIn.php | 253 +- extensions/authclient/clients/Twitter.php | 90 +- extensions/authclient/clients/YandexOAuth.php | 89 +- .../authclient/clients/YandexOpenId.php | 90 +- .../authclient/signature/BaseMethod.php | 55 +- extensions/authclient/signature/HmacSha1.php | 46 +- extensions/authclient/signature/PlainText.php | 28 +- extensions/authclient/signature/RsaSha1.php | 290 +- extensions/authclient/views/redirect.php | 27 +- extensions/authclient/widgets/Choice.php | 361 +- extensions/authclient/widgets/ChoiceAsset.php | 20 +- extensions/bootstrap/Alert.php | 180 +- extensions/bootstrap/BootstrapAsset.php | 8 +- extensions/bootstrap/BootstrapPluginAsset.php | 16 +- extensions/bootstrap/BootstrapThemeAsset.php | 14 +- extensions/bootstrap/Button.php | 61 +- extensions/bootstrap/ButtonDropdown.php | 167 +- extensions/bootstrap/ButtonGroup.php | 104 +- extensions/bootstrap/Carousel.php | 239 +- extensions/bootstrap/Collapse.php | 161 +- extensions/bootstrap/Dropdown.php | 140 +- extensions/bootstrap/Modal.php | 395 +- extensions/bootstrap/Nav.php | 329 +- extensions/bootstrap/NavBar.php | 227 +- extensions/bootstrap/Progress.php | 191 +- extensions/bootstrap/Tabs.php | 351 +- extensions/bootstrap/Widget.php | 107 +- extensions/codeception/BasePage.php | 100 +- extensions/codeception/DbTestCase.php | 18 +- extensions/codeception/TestCase.php | 193 +- extensions/composer/Installer.php | 404 +- extensions/composer/Plugin.php | 26 +- extensions/debug/DebugAsset.php | 18 +- extensions/debug/LogTarget.php | 299 +- extensions/debug/Module.php | 271 +- extensions/debug/Panel.php | 126 +- extensions/debug/components/search/Filter.php | 106 +- .../debug/components/search/matchers/Base.php | 36 +- .../search/matchers/GreaterThan.php | 14 +- .../components/search/matchers/LowerThan.php | 14 +- .../search/matchers/MatcherInterface.php | 40 +- .../components/search/matchers/SameAs.php | 30 +- .../debug/controllers/DefaultController.php | 263 +- extensions/debug/models/search/Base.php | 40 +- extensions/debug/models/search/Db.php | 110 +- extensions/debug/models/search/Debug.php | 236 +- extensions/debug/models/search/Log.php | 116 +- extensions/debug/models/search/Mail.php | 216 +- extensions/debug/models/search/Profile.php | 110 +- extensions/debug/panels/ConfigPanel.php | 145 +- extensions/debug/panels/DbPanel.php | 310 +- extensions/debug/panels/LogPanel.php | 128 +- extensions/debug/panels/MailPanel.php | 165 +- extensions/debug/panels/ProfilingPanel.php | 146 +- extensions/debug/panels/RequestPanel.php | 171 +- extensions/debug/views/default/index.php | 176 +- .../views/default/panels/config/detail.php | 36 +- .../views/default/panels/config/summary.php | 12 +- .../views/default/panels/config/table.php | 34 +- .../debug/views/default/panels/db/detail.php | 116 +- .../debug/views/default/panels/db/summary.php | 6 +- .../debug/views/default/panels/log/detail.php | 119 +- .../views/default/panels/log/summary.php | 16 +- .../debug/views/default/panels/mail/_item.php | 58 +- .../views/default/panels/mail/detail.php | 56 +- .../views/default/panels/mail/summary.php | 2 +- .../views/default/panels/profile/detail.php | 87 +- .../views/default/panels/profile/summary.php | 4 +- .../views/default/panels/request/detail.php | 54 +- .../views/default/panels/request/summary.php | 12 +- .../views/default/panels/request/table.php | 34 +- extensions/debug/views/default/toolbar.php | 20 +- extensions/debug/views/default/view.php | 110 +- extensions/debug/views/layouts/main.php | 8 +- extensions/elasticsearch/ActiveQuery.php | 409 +- extensions/elasticsearch/ActiveRecord.php | 1056 ++--- extensions/elasticsearch/Command.php | 739 ++-- extensions/elasticsearch/Connection.php | 673 +-- extensions/elasticsearch/DebugAction.php | 101 +- extensions/elasticsearch/DebugPanel.php | 266 +- extensions/elasticsearch/Exception.php | 14 +- extensions/elasticsearch/Query.php | 906 +++-- extensions/elasticsearch/QueryBuilder.php | 589 +-- extensions/faker/FixtureController.php | 461 +-- extensions/gii/CodeFile.php | 310 +- extensions/gii/Generator.php | 774 ++-- extensions/gii/GiiAsset.php | 52 +- extensions/gii/Module.php | 172 +- extensions/gii/components/ActiveField.php | 102 +- .../gii/components/DiffRendererHtmlInline.php | 205 +- .../gii/controllers/DefaultController.php | 202 +- .../gii/generators/controller/Generator.php | 431 +- .../controller/templates/controller.php | 8 +- .../generators/controller/templates/view.php | 4 +- extensions/gii/generators/crud/Generator.php | 945 ++--- extensions/gii/generators/crud/form.php | 4 +- .../generators/crud/templates/controller.php | 227 +- .../gii/generators/crud/templates/search.php | 92 +- .../generators/crud/templates/views/_form.php | 14 +- .../crud/templates/views/_search.php | 28 +- .../crud/templates/views/create.php | 8 +- .../generators/crud/templates/views/index.php | 70 +- .../crud/templates/views/update.php | 8 +- .../generators/crud/templates/views/view.php | 50 +- .../gii/generators/extension/Generator.php | 404 +- extensions/gii/generators/extension/form.php | 26 +- .../extension/templates/AutoloadExample.php | 6 +- extensions/gii/generators/form/Generator.php | 254 +- .../gii/generators/form/templates/action.php | 21 +- .../gii/generators/form/templates/form.php | 16 +- extensions/gii/generators/model/Generator.php | 1131 +++--- .../gii/generators/model/templates/model.php | 60 +- .../gii/generators/module/Generator.php | 273 +- extensions/gii/generators/module/form.php | 4 +- .../module/templates/controller.php | 8 +- .../generators/module/templates/module.php | 13 +- .../gii/generators/module/templates/view.php | 20 +- extensions/gii/views/default/diff.php | 14 +- extensions/gii/views/default/index.php | 28 +- extensions/gii/views/default/view.php | 88 +- extensions/gii/views/default/view/files.php | 166 +- extensions/gii/views/default/view/results.php | 16 +- extensions/gii/views/layouts/generator.php | 30 +- extensions/gii/views/layouts/main.php | 36 +- extensions/imagine/BaseImage.php | 448 +- extensions/jui/Accordion.php | 155 +- extensions/jui/AccordionAsset.php | 16 +- extensions/jui/AutoComplete.php | 40 +- extensions/jui/AutoCompleteAsset.php | 16 +- extensions/jui/ButtonAsset.php | 14 +- extensions/jui/CoreAsset.php | 20 +- extensions/jui/DatePicker.php | 143 +- extensions/jui/DatePickerAsset.php | 16 +- extensions/jui/DatePickerRegionalAsset.php | 14 +- extensions/jui/Dialog.php | 32 +- extensions/jui/DialogAsset.php | 20 +- extensions/jui/Draggable.php | 32 +- extensions/jui/DraggableAsset.php | 14 +- extensions/jui/Droppable.php | 32 +- extensions/jui/DroppableAsset.php | 14 +- extensions/jui/EffectAsset.php | 14 +- extensions/jui/InputWidget.php | 75 +- extensions/jui/Menu.php | 101 +- extensions/jui/MenuAsset.php | 14 +- extensions/jui/ProgressBar.php | 32 +- extensions/jui/ProgressBarAsset.php | 14 +- extensions/jui/Resizable.php | 32 +- extensions/jui/ResizableAsset.php | 14 +- extensions/jui/Selectable.php | 132 +- extensions/jui/SelectableAsset.php | 14 +- extensions/jui/Slider.php | 36 +- extensions/jui/SliderAsset.php | 14 +- extensions/jui/SliderInput.php | 90 +- extensions/jui/Sortable.php | 168 +- extensions/jui/SortableAsset.php | 14 +- extensions/jui/Spinner.php | 52 +- extensions/jui/SpinnerAsset.php | 16 +- extensions/jui/Tabs.php | 184 +- extensions/jui/TabsAsset.php | 16 +- extensions/jui/ThemeAsset.php | 8 +- extensions/jui/TooltipAsset.php | 16 +- extensions/jui/Widget.php | 221 +- extensions/mongodb/ActiveFixture.php | 177 +- extensions/mongodb/ActiveQuery.php | 218 +- extensions/mongodb/ActiveRecord.php | 630 +-- extensions/mongodb/Cache.php | 334 +- extensions/mongodb/Collection.php | 1878 ++++----- extensions/mongodb/Connection.php | 379 +- extensions/mongodb/Database.php | 284 +- extensions/mongodb/Exception.php | 14 +- extensions/mongodb/Query.php | 585 +-- extensions/mongodb/Session.php | 308 +- extensions/mongodb/file/ActiveQuery.php | 157 +- extensions/mongodb/file/ActiveRecord.php | 587 +-- extensions/mongodb/file/Collection.php | 323 +- extensions/mongodb/file/Query.php | 98 +- extensions/redis/ActiveQuery.php | 748 ++-- extensions/redis/ActiveRecord.php | 535 +-- extensions/redis/Cache.php | 289 +- extensions/redis/Connection.php | 707 ++-- extensions/redis/LuaScriptBuilder.php | 699 ++-- extensions/redis/Session.php | 177 +- extensions/smarty/ViewRenderer.php | 124 +- extensions/sphinx/ActiveQuery.php | 351 +- extensions/sphinx/ActiveRecord.php | 1250 +++--- extensions/sphinx/ColumnSchema.php | 119 +- extensions/sphinx/Command.php | 565 +-- extensions/sphinx/Connection.php | 127 +- extensions/sphinx/IndexSchema.php | 76 +- extensions/sphinx/Query.php | 1362 ++++--- extensions/sphinx/QueryBuilder.php | 1854 ++++----- extensions/sphinx/Schema.php | 931 ++--- extensions/swiftmailer/Mailer.php | 275 +- extensions/swiftmailer/Message.php | 568 +-- extensions/twig/TwigSimpleFileLoader.php | 101 +- extensions/twig/ViewRenderer.php | 414 +- .../twig/ViewRendererStaticClassProxy.php | 50 +- framework/BaseYii.php | 951 ++--- framework/base/Action.php | 141 +- framework/base/ActionEvent.php | 48 +- framework/base/ActionFilter.php | 155 +- framework/base/Application.php | 1214 +++--- framework/base/ArrayAccessTrait.php | 108 +- framework/base/Arrayable.php | 130 +- framework/base/ArrayableTrait.php | 265 +- framework/base/Behavior.php | 130 +- framework/base/Component.php | 1136 +++--- framework/base/Controller.php | 741 ++-- framework/base/DynamicModel.php | 260 +- framework/base/ErrorException.php | 141 +- framework/base/ErrorHandler.php | 601 +-- framework/base/Event.php | 307 +- framework/base/Exception.php | 14 +- framework/base/Extension.php | 14 +- framework/base/Formatter.php | 871 ++-- framework/base/InlineAction.php | 61 +- framework/base/InvalidCallException.php | 14 +- framework/base/InvalidConfigException.php | 14 +- framework/base/InvalidParamException.php | 14 +- framework/base/InvalidRouteException.php | 14 +- framework/base/MailEvent.php | 28 +- framework/base/Model.php | 1783 ++++---- framework/base/ModelEvent.php | 10 +- framework/base/Module.php | 1360 +++---- framework/base/NotSupportedException.php | 14 +- framework/base/Object.php | 394 +- framework/base/Request.php | 113 +- framework/base/Response.php | 46 +- framework/base/Theme.php | 144 +- framework/base/UnknownClassException.php | 14 +- framework/base/UnknownMethodException.php | 14 +- framework/base/UnknownPropertyException.php | 14 +- framework/base/View.php | 904 +++-- framework/base/ViewContextInterface.php | 12 +- framework/base/ViewEvent.php | 26 +- framework/base/ViewRenderer.php | 24 +- framework/base/Widget.php | 381 +- framework/behaviors/AttributeBehavior.php | 117 +- framework/behaviors/BlameableBehavior.php | 83 +- framework/behaviors/TimestampBehavior.php | 85 +- framework/caching/ApcCache.php | 205 +- framework/caching/Cache.php | 844 ++-- framework/caching/ChainedDependency.php | 101 +- framework/caching/DbCache.php | 446 +- framework/caching/DbDependency.php | 79 +- framework/caching/Dependency.php | 144 +- framework/caching/DummyCache.php | 108 +- framework/caching/ExpressionDependency.php | 42 +- framework/caching/FileCache.php | 447 +- framework/caching/FileDependency.php | 39 +- framework/caching/GroupDependency.php | 99 +- framework/caching/MemCache.php | 376 +- framework/caching/MemCacheServer.php | 74 +- framework/caching/WinCache.php | 203 +- framework/caching/XCache.php | 152 +- framework/caching/ZendDataCache.php | 113 +- framework/captcha/Captcha.php | 194 +- framework/captcha/CaptchaAction.php | 606 +-- framework/captcha/CaptchaAsset.php | 14 +- framework/captcha/CaptchaValidator.php | 143 +- framework/classes.php | 522 +-- framework/console/Application.php | 281 +- framework/console/Controller.php | 451 ++- framework/console/Exception.php | 14 +- framework/console/Request.php | 103 +- .../console/controllers/AssetController.php | 1118 ++--- .../console/controllers/CacheController.php | 82 +- .../console/controllers/FixtureController.php | 568 +-- .../console/controllers/HelpController.php | 867 ++-- .../console/controllers/MessageController.php | 640 +-- .../console/controllers/MigrateController.php | 1251 +++--- framework/data/ActiveDataProvider.php | 246 +- framework/data/ArrayDataProvider.php | 141 +- framework/data/BaseDataProvider.php | 438 +- framework/data/DataProviderInterface.php | 86 +- framework/data/Pagination.php | 502 +-- framework/data/Sort.php | 599 +-- framework/data/SqlDataProvider.php | 174 +- framework/db/ActiveQuery.php | 1087 ++--- framework/db/ActiveQueryInterface.php | 140 +- framework/db/ActiveQueryTrait.php | 359 +- framework/db/ActiveRecord.php | 1069 ++--- framework/db/ActiveRecordInterface.php | 552 +-- framework/db/ActiveRelationTrait.php | 805 ++-- framework/db/BaseActiveRecord.php | 2729 ++++++------- framework/db/BatchQueryResult.php | 266 +- framework/db/ColumnSchema.php | 168 +- framework/db/Command.php | 1426 +++---- framework/db/Connection.php | 835 ++-- framework/db/DataReader.php | 389 +- framework/db/Exception.php | 58 +- framework/db/Expression.php | 60 +- framework/db/Migration.php | 760 ++-- framework/db/Query.php | 1539 +++---- framework/db/QueryBuilder.php | 2260 ++++++----- framework/db/QueryInterface.php | 338 +- framework/db/QueryTrait.php | 379 +- framework/db/Schema.php | 861 ++-- framework/db/StaleObjectException.php | 14 +- framework/db/TableSchema.php | 157 +- framework/db/Transaction.php | 205 +- framework/db/cubrid/QueryBuilder.php | 138 +- framework/db/cubrid/Schema.php | 457 +-- framework/db/mssql/PDO.php | 87 +- framework/db/mssql/QueryBuilder.php | 201 +- framework/db/mssql/Schema.php | 670 +-- framework/db/mssql/SqlsrvPDO.php | 28 +- framework/db/mssql/TableSchema.php | 10 +- framework/db/mysql/QueryBuilder.php | 273 +- framework/db/mysql/Schema.php | 504 +-- framework/db/oci/QueryBuilder.php | 220 +- framework/db/oci/Schema.php | 377 +- framework/db/pgsql/QueryBuilder.php | 240 +- framework/db/pgsql/Schema.php | 714 ++-- framework/db/sqlite/QueryBuilder.php | 475 +-- framework/db/sqlite/Schema.php | 455 +-- framework/grid/ActionColumn.php | 223 +- framework/grid/CheckboxColumn.php | 128 +- framework/grid/Column.php | 286 +- framework/grid/DataColumn.php | 273 +- framework/grid/GridView.php | 879 ++-- framework/grid/GridViewAsset.php | 14 +- framework/grid/SerialColumn.php | 26 +- framework/helpers/BaseArrayHelper.php | 899 +++-- framework/helpers/BaseConsole.php | 1812 ++++----- framework/helpers/BaseFileHelper.php | 1071 ++--- framework/helpers/BaseHtml.php | 3594 +++++++++-------- framework/helpers/BaseHtmlPurifier.php | 31 +- framework/helpers/BaseInflector.php | 937 ++--- framework/helpers/BaseJson.php | 174 +- framework/helpers/BaseMarkdown.php | 148 +- framework/helpers/BaseSecurity.php | 676 ++-- framework/helpers/BaseStringHelper.php | 131 +- framework/helpers/BaseUrl.php | 411 +- framework/helpers/BaseVarDumper.php | 205 +- framework/helpers/Url.php | 1 - framework/helpers/mimeTypes.php | 344 +- framework/i18n/DbMessageSource.php | 232 +- framework/i18n/Formatter.php | 547 +-- framework/i18n/GettextFile.php | 32 +- framework/i18n/GettextMessageSource.php | 191 +- framework/i18n/GettextMoFile.php | 453 +-- framework/i18n/GettextPoFile.php | 148 +- framework/i18n/I18N.php | 341 +- framework/i18n/MessageFormatter.php | 681 ++-- framework/i18n/MessageSource.php | 185 +- framework/i18n/MissingTranslationEvent.php | 36 +- framework/i18n/PhpMessageSource.php | 179 +- framework/log/DbTarget.php | 128 +- framework/log/EmailTarget.php | 107 +- framework/log/FileTarget.php | 191 +- framework/log/Logger.php | 523 +-- framework/log/Target.php | 410 +- framework/mail/BaseMailer.php | 613 +-- framework/mail/BaseMessage.php | 52 +- framework/mail/MailerInterface.php | 62 +- framework/mail/MessageInterface.php | 366 +- framework/messages/ar/yii.php | 122 +- framework/messages/config.php | 94 +- framework/messages/fa-IR/yii.php | 2 +- framework/messages/ro/yii.php | 2 +- framework/messages/uk/yii.php | 122 +- framework/mutex/DbMutex.php | 40 +- framework/mutex/FileMutex.php | 162 +- framework/mutex/Mutex.php | 141 +- framework/mutex/MysqlMutex.php | 72 +- framework/rbac/Assignment.php | 58 +- framework/rbac/DbManager.php | 1140 +++--- framework/rbac/Item.php | 348 +- framework/rbac/Manager.php | 518 +-- framework/rbac/PhpManager.php | 1050 ++--- .../requirements/YiiRequirementChecker.php | 664 +-- framework/requirements/requirements.php | 82 +- .../requirements/views/console/index.php | 24 +- framework/requirements/views/web/index.php | 122 +- framework/rest/Action.php | 157 +- framework/rest/ActiveController.php | 167 +- framework/rest/AuthInterface.php | 34 +- framework/rest/Controller.php | 429 +- framework/rest/CreateAction.php | 103 +- framework/rest/DeleteAction.php | 63 +- framework/rest/HttpBasicAuth.php | 53 +- framework/rest/HttpBearerAuth.php | 55 +- framework/rest/IndexAction.php | 82 +- framework/rest/OptionsAction.php | 41 +- framework/rest/QueryParamAuth.php | 57 +- framework/rest/RateLimitInterface.php | 42 +- framework/rest/RateLimiter.php | 108 +- framework/rest/Serializer.php | 446 +- framework/rest/UpdateAction.php | 79 +- framework/rest/UrlRule.php | 379 +- framework/rest/ViewAction.php | 27 +- framework/test/ActiveFixture.php | 233 +- framework/test/BaseActiveFixture.php | 156 +- framework/test/DbFixture.php | 39 +- framework/test/Fixture.php | 91 +- framework/test/FixtureTrait.php | 358 +- framework/test/InitDbFixture.php | 123 +- framework/validators/BooleanValidator.php | 127 +- framework/validators/CompareValidator.php | 325 +- framework/validators/DateValidator.php | 99 +- .../validators/DefaultValueValidator.php | 36 +- framework/validators/EmailValidator.php | 175 +- framework/validators/ExistValidator.php | 185 +- framework/validators/FileValidator.php | 461 +-- framework/validators/FilterValidator.php | 62 +- framework/validators/ImageValidator.php | 334 +- framework/validators/InlineValidator.php | 111 +- framework/validators/NumberValidator.php | 237 +- framework/validators/PunycodeAsset.php | 8 +- framework/validators/RangeValidator.php | 116 +- .../validators/RegularExpressionValidator.php | 128 +- framework/validators/RequiredValidator.php | 171 +- framework/validators/SafeValidator.php | 12 +- framework/validators/StringValidator.php | 329 +- framework/validators/UniqueValidator.php | 179 +- framework/validators/UrlValidator.php | 212 +- framework/validators/ValidationAsset.php | 14 +- framework/validators/Validator.php | 496 +-- .../views/errorHandler/callStackItem.php | 60 +- framework/views/errorHandler/error.php | 104 +- framework/views/errorHandler/exception.php | 482 --- .../views/errorHandler/previousException.php | 26 +- framework/views/messageConfig.php | 96 +- framework/views/migration.php | 19 +- framework/web/AccessControl.php | 170 +- framework/web/AccessRule.php | 315 +- framework/web/Application.php | 292 +- framework/web/AssetBundle.php | 320 +- framework/web/AssetConverter.php | 147 +- framework/web/AssetConverterInterface.php | 14 +- framework/web/AssetManager.php | 582 +-- framework/web/BadRequestHttpException.php | 20 +- framework/web/CacheSession.php | 143 +- framework/web/CompositeUrlRule.php | 92 +- framework/web/ConflictHttpException.php | 20 +- framework/web/Controller.php | 344 +- framework/web/Cookie.php | 92 +- framework/web/CookieCollection.php | 400 +- framework/web/DbSession.php | 374 +- framework/web/ErrorAction.php | 99 +- framework/web/ForbiddenHttpException.php | 20 +- framework/web/GoneHttpException.php | 20 +- framework/web/HeaderCollection.php | 395 +- framework/web/HttpCache.php | 210 +- framework/web/HttpException.php | 54 +- framework/web/IdentityInterface.php | 86 +- framework/web/JqueryAsset.php | 8 +- framework/web/JsExpression.php | 44 +- framework/web/JsonParser.php | 52 +- framework/web/Link.php | 104 +- framework/web/Linkable.php | 48 +- .../web/MethodNotAllowedHttpException.php | 20 +- framework/web/NotAcceptableHttpException.php | 20 +- framework/web/NotFoundHttpException.php | 20 +- framework/web/PageCache.php | 183 +- framework/web/Request.php | 2476 ++++++------ framework/web/RequestParserInterface.php | 14 +- framework/web/Response.php | 1643 ++++---- framework/web/ResponseFormatterInterface.php | 10 +- framework/web/Session.php | 1382 +++---- framework/web/SessionIterator.php | 120 +- .../web/TooManyRequestsHttpException.php | 20 +- framework/web/UnauthorizedHttpException.php | 20 +- .../web/UnsupportedMediaTypeHttpException.php | 20 +- framework/web/UploadedFile.php | 415 +- framework/web/UrlManager.php | 579 +-- framework/web/UrlRule.php | 564 +-- framework/web/UrlRuleInterface.php | 32 +- framework/web/User.php | 1064 ++--- framework/web/UserEvent.php | 40 +- framework/web/VerbFilter.php | 111 +- framework/web/View.php | 1006 ++--- framework/web/XmlResponseFormatter.php | 138 +- framework/web/YiiAsset.php | 14 +- framework/widgets/ActiveField.php | 1308 +++--- framework/widgets/ActiveForm.php | 655 +-- framework/widgets/ActiveFormAsset.php | 14 +- framework/widgets/BaseListView.php | 409 +- framework/widgets/Block.php | 50 +- framework/widgets/Breadcrumbs.php | 182 +- framework/widgets/ContentDecorator.php | 62 +- framework/widgets/DetailView.php | 311 +- framework/widgets/FragmentCache.php | 291 +- framework/widgets/InputWidget.php | 85 +- framework/widgets/LinkPager.php | 388 +- framework/widgets/LinkSorter.php | 90 +- framework/widgets/ListView.php | 171 +- framework/widgets/MaskedInput.php | 177 +- framework/widgets/MaskedInputAsset.php | 14 +- framework/widgets/Menu.php | 514 +-- framework/widgets/Pjax.php | 270 +- framework/widgets/PjaxAsset.php | 14 +- framework/widgets/Spaceless.php | 32 +- tests/unit/TestCase.php | 101 +- tests/unit/VendorTestCase.php | 40 +- tests/unit/bootstrap.php | 2 +- tests/unit/data/ar/ActiveRecord.php | 10 +- tests/unit/data/ar/Category.php | 16 +- tests/unit/data/ar/Customer.php | 82 +- tests/unit/data/ar/CustomerQuery.php | 11 +- tests/unit/data/ar/Item.php | 16 +- tests/unit/data/ar/NullValues.php | 8 +- tests/unit/data/ar/Order.php | 117 +- tests/unit/data/ar/OrderItem.php | 24 +- tests/unit/data/ar/Profile.php | 8 +- .../data/ar/elasticsearch/ActiveRecord.php | 24 +- tests/unit/data/ar/elasticsearch/Customer.php | 93 +- .../data/ar/elasticsearch/CustomerQuery.php | 11 +- tests/unit/data/ar/elasticsearch/Item.php | 50 +- tests/unit/data/ar/elasticsearch/Order.php | 120 +- .../unit/data/ar/elasticsearch/OrderItem.php | 60 +- tests/unit/data/ar/mongodb/ActiveRecord.php | 10 +- tests/unit/data/ar/mongodb/Customer.php | 47 +- tests/unit/data/ar/mongodb/CustomerOrder.php | 35 +- tests/unit/data/ar/mongodb/CustomerQuery.php | 11 +- .../data/ar/mongodb/file/ActiveRecord.php | 10 +- .../data/ar/mongodb/file/CustomerFile.php | 39 +- .../ar/mongodb/file/CustomerFileQuery.php | 11 +- tests/unit/data/ar/redis/ActiveRecord.php | 10 +- tests/unit/data/ar/redis/Customer.php | 61 +- tests/unit/data/ar/redis/CustomerQuery.php | 11 +- tests/unit/data/ar/redis/Item.php | 8 +- tests/unit/data/ar/redis/Order.php | 111 +- tests/unit/data/ar/redis/OrderItem.php | 32 +- tests/unit/data/ar/sphinx/ActiveRecord.php | 10 +- tests/unit/data/ar/sphinx/ArticleDb.php | 29 +- tests/unit/data/ar/sphinx/ArticleIndex.php | 45 +- .../unit/data/ar/sphinx/ArticleIndexQuery.php | 11 +- tests/unit/data/ar/sphinx/ItemDb.php | 8 +- tests/unit/data/ar/sphinx/ItemIndex.php | 8 +- tests/unit/data/ar/sphinx/RuntimeIndex.php | 8 +- tests/unit/data/ar/sphinx/TagDb.php | 8 +- tests/unit/data/base/InvalidRulesModel.php | 12 +- tests/unit/data/base/Singer.php | 20 +- tests/unit/data/base/Speaker.php | 68 +- tests/unit/data/config.php | 114 +- tests/unit/data/i18n/messages/de-DE/test.php | 6 +- tests/unit/data/i18n/messages/de/test.php | 2 +- tests/unit/data/i18n/messages/en-US/test.php | 2 +- tests/unit/data/i18n/messages/ru/test.php | 2 +- tests/unit/data/validators/TestValidator.php | 69 +- .../models/FakedValidationModel.php | 99 +- .../models/ValidatorTestMainModel.php | 19 +- .../models/ValidatorTestRefModel.php | 21 +- tests/unit/data/views/layout.php | 4 +- tests/unit/data/views/simple.php | 2 +- .../extensions/authclient/AuthActionTest.php | 97 +- .../extensions/authclient/BaseClientTest.php | 118 +- .../extensions/authclient/BaseOAuthTest.php | 480 +-- .../extensions/authclient/CollectionTest.php | 118 +- .../unit/extensions/authclient/OAuth1Test.php | 191 +- .../unit/extensions/authclient/OAuth2Test.php | 54 +- .../unit/extensions/authclient/OpenIdTest.php | 104 +- tests/unit/extensions/authclient/TestCase.php | 30 +- .../unit/extensions/authclient/TokenTest.php | 246 +- .../authclient/signature/BaseMethodTest.php | 83 +- .../authclient/signature/HmacSha1Test.php | 16 +- .../authclient/signature/PlainTextTest.php | 16 +- .../authclient/signature/RsaSha1Test.php | 112 +- .../elasticsearch/ActiveRecordTest.php | 1060 ++--- .../ElasticSearchConnectionTest.php | 30 +- .../elasticsearch/ElasticSearchTestCase.php | 71 +- .../elasticsearch/QueryBuilderTest.php | 112 +- .../extensions/elasticsearch/QueryTest.php | 352 +- .../extensions/imagine/AbstractImageTest.php | 170 +- tests/unit/extensions/imagine/ImageGdTest.php | 31 +- .../extensions/imagine/ImageGmagickTest.php | 28 +- .../extensions/imagine/ImageImagickTest.php | 28 +- .../mongodb/ActiveDataProviderTest.php | 136 +- .../extensions/mongodb/ActiveRecordTest.php | 504 +-- .../extensions/mongodb/ActiveRelationTest.php | 130 +- tests/unit/extensions/mongodb/CacheTest.php | 250 +- .../extensions/mongodb/CollectionTest.php | 912 ++--- .../extensions/mongodb/ConnectionTest.php | 208 +- .../unit/extensions/mongodb/DatabaseTest.php | 90 +- .../extensions/mongodb/MongoDbTestCase.php | 257 +- .../unit/extensions/mongodb/QueryRunTest.php | 264 +- tests/unit/extensions/mongodb/QueryTest.php | 170 +- tests/unit/extensions/mongodb/SessionTest.php | 260 +- .../mongodb/file/ActiveRecordTest.php | 614 +-- .../mongodb/file/CollectionTest.php | 172 +- .../extensions/mongodb/file/QueryTest.php | 102 +- .../extensions/redis/ActiveRecordTest.php | 477 ++- .../unit/extensions/redis/RedisCacheTest.php | 165 +- .../extensions/redis/RedisConnectionTest.php | 82 +- tests/unit/extensions/redis/RedisTestCase.php | 65 +- .../extensions/smarty/ViewRendererTest.php | 93 +- .../sphinx/ActiveDataProviderTest.php | 90 +- .../extensions/sphinx/ActiveRecordTest.php | 444 +- .../extensions/sphinx/ActiveRelationTest.php | 54 +- .../extensions/sphinx/ColumnSchemaTest.php | 84 +- tests/unit/extensions/sphinx/CommandTest.php | 794 ++-- .../unit/extensions/sphinx/ConnectionTest.php | 60 +- .../sphinx/ExternalActiveRelationTest.php | 102 +- tests/unit/extensions/sphinx/QueryTest.php | 372 +- tests/unit/extensions/sphinx/SchemaTest.php | 118 +- .../unit/extensions/sphinx/SphinxTestCase.php | 266 +- .../extensions/swiftmailer/MailerTest.php | 213 +- .../extensions/swiftmailer/MessageTest.php | 689 ++-- .../unit/extensions/twig/ViewRendererTest.php | 102 +- tests/unit/framework/BaseYiiTest.php | 100 +- .../framework/ar/ActiveRecordTestTrait.php | 1759 ++++---- tests/unit/framework/base/BehaviorTest.php | 160 +- tests/unit/framework/base/ComponentTest.php | 833 ++-- .../unit/framework/base/DynamicModelTest.php | 112 +- tests/unit/framework/base/EventTest.php | 110 +- tests/unit/framework/base/FormatterTest.php | 366 +- tests/unit/framework/base/ModelTest.php | 500 +-- tests/unit/framework/base/ObjectTest.php | 365 +- .../behaviors/TimestampBehaviorTest.php | 160 +- tests/unit/framework/caching/ApcCacheTest.php | 65 +- .../unit/framework/caching/CacheTestCase.php | 433 +- tests/unit/framework/caching/DbCacheTest.php | 151 +- .../unit/framework/caching/FileCacheTest.php | 73 +- tests/unit/framework/caching/MemCacheTest.php | 57 +- .../unit/framework/caching/MemCachedTest.php | 57 +- tests/unit/framework/caching/WinCacheTest.php | 35 +- tests/unit/framework/caching/XCacheTest.php | 29 +- .../framework/caching/ZendDataCacheTest.php | 29 +- .../controllers/AssetControllerTest.php | 694 ++-- .../controllers/MessageControllerTest.php | 712 ++-- .../framework/data/ActiveDataProviderTest.php | 306 +- tests/unit/framework/data/SortTest.php | 312 +- tests/unit/framework/db/ActiveRecordTest.php | 940 ++--- .../framework/db/BatchQueryResultTest.php | 208 +- tests/unit/framework/db/CommandTest.php | 530 +-- tests/unit/framework/db/ConnectionTest.php | 114 +- tests/unit/framework/db/DatabaseTestCase.php | 111 +- tests/unit/framework/db/QueryBuilderTest.php | 446 +- tests/unit/framework/db/QueryTest.php | 226 +- tests/unit/framework/db/SchemaTest.php | 134 +- .../cubrid/CubridActiveDataProviderTest.php | 2 +- .../db/cubrid/CubridActiveRecordTest.php | 2 +- .../framework/db/cubrid/CubridCommandTest.php | 116 +- .../db/cubrid/CubridConnectionTest.php | 58 +- .../db/cubrid/CubridQueryBuilderTest.php | 134 +- .../framework/db/cubrid/CubridQueryTest.php | 2 +- .../framework/db/cubrid/CubridSchemaTest.php | 42 +- .../db/mssql/MssqlActiveDataProviderTest.php | 2 +- .../db/mssql/MssqlActiveRecordTest.php | 2 +- .../framework/db/mssql/MssqlCommandTest.php | 122 +- .../db/mssql/MssqlConnectionTest.php | 58 +- .../db/mssql/MssqlQueryBuilderTest.php | 64 +- .../framework/db/mssql/MssqlQueryTest.php | 2 +- .../PostgreSQLActiveDataProviderTest.php | 2 +- .../db/pgsql/PostgreSQLActiveRecordTest.php | 2 +- .../db/pgsql/PostgreSQLConnectionTest.php | 70 +- .../db/pgsql/PostgreSQLQueryBuilderTest.php | 126 +- .../sqlite/SqliteActiveDataProviderTest.php | 2 +- .../db/sqlite/SqliteActiveRecordTest.php | 2 +- .../framework/db/sqlite/SqliteCommandTest.php | 16 +- .../db/sqlite/SqliteConnectionTest.php | 62 +- .../db/sqlite/SqliteQueryBuilderTest.php | 148 +- .../framework/db/sqlite/SqliteQueryTest.php | 2 +- .../framework/db/sqlite/SqliteSchemaTest.php | 2 +- .../framework/helpers/ArrayHelperTest.php | 705 ++-- tests/unit/framework/helpers/ConsoleTest.php | 130 +- .../unit/framework/helpers/FileHelperTest.php | 746 ++-- tests/unit/framework/helpers/HtmlTest.php | 966 ++--- .../unit/framework/helpers/InflectorTest.php | 266 +- tests/unit/framework/helpers/JsonTest.php | 93 +- tests/unit/framework/helpers/SecurityTest.php | 52 +- .../framework/helpers/StringHelperTest.php | 88 +- tests/unit/framework/helpers/UrlTest.php | 308 +- .../unit/framework/helpers/VarDumperTest.php | 16 +- .../i18n/FallbackMessageFormatterTest.php | 284 +- tests/unit/framework/i18n/FormatterTest.php | 134 +- .../i18n/GettextMessageSourceTest.php | 8 +- .../unit/framework/i18n/GettextMoFileTest.php | 170 +- .../unit/framework/i18n/GettextPoFileTest.php | 170 +- tests/unit/framework/i18n/I18NTest.php | 207 +- .../framework/i18n/MessageFormatterTest.php | 580 +-- tests/unit/framework/log/LoggerTest.php | 29 +- tests/unit/framework/log/TargetTest.php | 123 +- tests/unit/framework/mail/BaseMailerTest.php | 705 ++-- tests/unit/framework/mail/BaseMessageTest.php | 203 +- tests/unit/framework/rbac/ManagerTestCase.php | 508 +-- tests/unit/framework/rbac/PhpManagerTest.php | 46 +- .../YiiRequirementCheckerTest.php | 366 +- .../unit/framework/test/ActiveFixtureTest.php | 114 +- tests/unit/framework/test/FixtureTest.php | 272 +- .../unit/framework/test/data/tbl_customer.php | 24 +- .../validators/BooleanValidatorTest.php | 88 +- .../validators/CompareValidatorTest.php | 310 +- .../validators/DateValidatorTest.php | 108 +- .../validators/DefaultValueValidatorTest.php | 52 +- .../validators/EmailValidatorTest.php | 165 +- .../ExistValidatorPostgresTest.php | 3 +- .../ExistValidatorSQliteTest.php | 3 +- .../validators/ExistValidatorTest.php | 223 +- .../validators/FileValidatorTest.php | 623 +-- .../validators/FilterValidatorTest.php | 75 +- .../validators/NumberValidatorTest.php | 293 +- .../validators/RangeValidatorTest.php | 117 +- .../RegularExpressionValidatorTest.php | 83 +- .../validators/RequiredValidatorTest.php | 93 +- .../validators/StringValidatorTest.php | 197 +- .../UniqueValidatorPostgresTest.php | 3 +- .../UniqueValidatorSQliteTest.php | 3 +- .../validators/UniqueValidatorTest.php | 219 +- .../framework/validators/UrlValidatorTest.php | 173 +- .../framework/validators/ValidatorTest.php | 436 +- tests/unit/framework/web/AssetBundleTest.php | 393 +- .../unit/framework/web/AssetConverterTest.php | 41 +- tests/unit/framework/web/CacheSessionTest.php | 38 +- tests/unit/framework/web/RequestTest.php | 38 +- tests/unit/framework/web/ResponseTest.php | 120 +- tests/unit/framework/web/UrlManagerTest.php | 562 +-- tests/unit/framework/web/UrlRuleTest.php | 1338 +++--- .../web/XmlResponseFormatterTest.php | 210 +- .../unit/framework/widgets/ActiveFormTest.php | 40 +- .../unit/framework/widgets/SpacelessTest.php | 58 +- .../protected/controllers/SiteController.php | 46 +- 917 files changed, 96153 insertions(+), 95419 deletions(-) diff --git a/apps/advanced/backend/assets/AppAsset.php b/apps/advanced/backend/assets/AppAsset.php index bd5c3a0dfb5..b6c32686051 100644 --- a/apps/advanced/backend/assets/AppAsset.php +++ b/apps/advanced/backend/assets/AppAsset.php @@ -15,12 +15,12 @@ */ class AppAsset extends AssetBundle { - public $basePath = '@webroot'; - public $baseUrl = '@web'; - public $css = ['css/site.css']; - public $js = []; - public $depends = [ - 'yii\web\YiiAsset', - 'yii\bootstrap\BootstrapAsset', - ]; + public $basePath = '@webroot'; + public $baseUrl = '@web'; + public $css = ['css/site.css']; + public $js = []; + public $depends = [ + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ]; } diff --git a/apps/advanced/backend/config/main.php b/apps/advanced/backend/config/main.php index 6255b8f95b0..1eaa1ceda8e 100644 --- a/apps/advanced/backend/config/main.php +++ b/apps/advanced/backend/config/main.php @@ -1,34 +1,34 @@ 'app-backend', - 'basePath' => dirname(__DIR__), - 'preload' => ['log'], - 'controllerNamespace' => 'backend\controllers', - 'modules' => [], - 'components' => [ - 'user' => [ - 'identityClass' => 'common\models\User', - 'enableAutoLogin' => true, - ], - 'log' => [ - 'traceLevel' => YII_DEBUG ? 3 : 0, - 'targets' => [ - [ - 'class' => 'yii\log\FileTarget', - 'levels' => ['error', 'warning'], - ], - ], - ], - 'errorHandler' => [ - 'errorAction' => 'site/error', - ], - ], - 'params' => $params, + 'id' => 'app-backend', + 'basePath' => dirname(__DIR__), + 'preload' => ['log'], + 'controllerNamespace' => 'backend\controllers', + 'modules' => [], + 'components' => [ + 'user' => [ + 'identityClass' => 'common\models\User', + 'enableAutoLogin' => true, + ], + 'log' => [ + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + 'errorHandler' => [ + 'errorAction' => 'site/error', + ], + ], + 'params' => $params, ]; diff --git a/apps/advanced/backend/config/params.php b/apps/advanced/backend/config/params.php index 0e625dc7d18..7f754b91fe0 100644 --- a/apps/advanced/backend/config/params.php +++ b/apps/advanced/backend/config/params.php @@ -1,4 +1,4 @@ 'admin@example.com', + 'adminEmail' => 'admin@example.com', ]; diff --git a/apps/advanced/backend/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php index 60d3c1bf0c1..2999218ab36 100644 --- a/apps/advanced/backend/controllers/SiteController.php +++ b/apps/advanced/backend/controllers/SiteController.php @@ -12,71 +12,72 @@ */ class SiteController extends Controller { - /** - * @inheritdoc - */ - public function behaviors() - { - return [ - 'access' => [ - 'class' => AccessControl::className(), - 'rules' => [ - [ - 'actions' => ['login', 'error'], - 'allow' => true, - ], - [ - 'actions' => ['logout', 'index'], - 'allow' => true, - 'roles' => ['@'], - ], - ], - ], - 'verbs' => [ - 'class' => VerbFilter::className(), - 'actions' => [ - 'logout' => ['post'], - ], - ], - ]; - } + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + 'access' => [ + 'class' => AccessControl::className(), + 'rules' => [ + [ + 'actions' => ['login', 'error'], + 'allow' => true, + ], + [ + 'actions' => ['logout', 'index'], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], + ]; + } - /** - * @inheritdoc - */ - public function actions() - { - return [ - 'error' => [ - 'class' => 'yii\web\ErrorAction', - ], - ]; - } + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'error' => [ + 'class' => 'yii\web\ErrorAction', + ], + ]; + } - public function actionIndex() - { - return $this->render('index'); - } + public function actionIndex() + { + return $this->render('index'); + } - public function actionLogin() - { - if (!\Yii::$app->user->isGuest) { - return $this->goHome(); - } + public function actionLogin() + { + if (!\Yii::$app->user->isGuest) { + return $this->goHome(); + } - $model = new LoginForm(); - if ($model->load(Yii::$app->request->post()) && $model->login()) { - return $this->goBack(); - } else { - return $this->render('login', [ - 'model' => $model, - ]); - } - } + $model = new LoginForm(); + if ($model->load(Yii::$app->request->post()) && $model->login()) { + return $this->goBack(); + } else { + return $this->render('login', [ + 'model' => $model, + ]); + } + } - public function actionLogout() - { - Yii::$app->user->logout(); - return $this->goHome(); - } + public function actionLogout() + { + Yii::$app->user->logout(); + + return $this->goHome(); + } } diff --git a/apps/advanced/backend/tests/_config.php b/apps/advanced/backend/tests/_config.php index d9cc356dd80..24ec1b56aec 100644 --- a/apps/advanced/backend/tests/_config.php +++ b/apps/advanced/backend/tests/_config.php @@ -3,12 +3,12 @@ * application configurations shared by all test types */ return [ - 'components' => [ - 'mail' => [ - 'useFileTransport' => true, - ], - 'urlManager' => [ - 'showScriptName' => true, - ], - ], + 'components' => [ + 'mail' => [ + 'useFileTransport' => true, + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], ]; diff --git a/apps/advanced/backend/tests/_helpers/CodeHelper.php b/apps/advanced/backend/tests/_helpers/CodeHelper.php index 972c8f3f1ad..eea532c3b7b 100644 --- a/apps/advanced/backend/tests/_helpers/CodeHelper.php +++ b/apps/advanced/backend/tests/_helpers/CodeHelper.php @@ -1,7 +1,7 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', - ], - ], - ] + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', + ], + ], + ] ); diff --git a/apps/advanced/backend/tests/acceptance/_console.php b/apps/advanced/backend/tests/acceptance/_console.php index 1e1ec5611c5..bae7e44b03f 100644 --- a/apps/advanced/backend/tests/acceptance/_console.php +++ b/apps/advanced/backend/tests/acceptance/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', + ], + ], + ] ); diff --git a/apps/advanced/backend/tests/functional/_config.php b/apps/advanced/backend/tests/functional/_config.php index c1bc080f567..ba776b6169f 100644 --- a/apps/advanced/backend/tests/functional/_config.php +++ b/apps/advanced/backend/tests/functional/_config.php @@ -5,16 +5,16 @@ $_SERVER['SCRIPT_NAME'] = TEST_ENTRY_URL; return yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../config/main.php'), - require(__DIR__ . '/../../config/main-local.php'), - require(__DIR__ . '/../../../common/config/main.php'), - require(__DIR__ . '/../../../common/config/main-local.php'), - require(__DIR__ . '/../_config.php'), - [ - 'components' => [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', - ], - ], - ] + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', + ], + ], + ] ); diff --git a/apps/advanced/backend/tests/functional/_console.php b/apps/advanced/backend/tests/functional/_console.php index 39434f7a9d1..d76662c4e51 100644 --- a/apps/advanced/backend/tests/functional/_console.php +++ b/apps/advanced/backend/tests/functional/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', + ], + ], + ] ); diff --git a/apps/advanced/backend/tests/unit/DbTestCase.php b/apps/advanced/backend/tests/unit/DbTestCase.php index 4cb8ff357d3..6c400201c51 100644 --- a/apps/advanced/backend/tests/unit/DbTestCase.php +++ b/apps/advanced/backend/tests/unit/DbTestCase.php @@ -4,5 +4,5 @@ class DbTestCase extends \yii\codeception\DbTestCase { - public $appConfig = '@backend/tests/unit/_config.php'; + public $appConfig = '@backend/tests/unit/_config.php'; } diff --git a/apps/advanced/backend/tests/unit/TestCase.php b/apps/advanced/backend/tests/unit/TestCase.php index fdd63dd5021..832f36e3890 100644 --- a/apps/advanced/backend/tests/unit/TestCase.php +++ b/apps/advanced/backend/tests/unit/TestCase.php @@ -4,5 +4,5 @@ class TestCase extends \yii\codeception\TestCase { - public $appConfig = '@backend/tests/unit/_config.php'; + public $appConfig = '@backend/tests/unit/_config.php'; } diff --git a/apps/advanced/backend/tests/unit/_config.php b/apps/advanced/backend/tests/unit/_config.php index 22e5c6245c8..97ed57b0f23 100644 --- a/apps/advanced/backend/tests/unit/_config.php +++ b/apps/advanced/backend/tests/unit/_config.php @@ -1,16 +1,16 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/backend/tests/unit/_console.php b/apps/advanced/backend/tests/unit/_console.php index a0d8d02284f..1060e27de4e 100644 --- a/apps/advanced/backend/tests/unit/_console.php +++ b/apps/advanced/backend/tests/unit/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/backend/views/layouts/main.php b/apps/advanced/backend/views/layouts/main.php index 0a1bf133216..4122a9f8a65 100644 --- a/apps/advanced/backend/views/layouts/main.php +++ b/apps/advanced/backend/views/layouts/main.php @@ -15,57 +15,57 @@ - - - <?= Html::encode($this->title) ?> - head() ?> + + + <?= Html::encode($this->title) ?> + head() ?> - beginBody() ?> -
- 'My Company', - 'brandUrl' => Yii::$app->homeUrl, - 'options' => [ - 'class' => 'navbar-inverse navbar-fixed-top', - ], - ]); - $menuItems = [ - ['label' => 'Home', 'url' => ['/site/index']], - ]; - if (Yii::$app->user->isGuest) { - $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; - } else { - $menuItems[] = [ - 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', - 'url' => ['/site/logout'], - 'linkOptions' => ['data-method' => 'post'] - ]; - } - echo Nav::widget([ - 'options' => ['class' => 'navbar-nav navbar-right'], - 'items' => $menuItems, - ]); - NavBar::end(); - ?> + beginBody() ?> +
+ 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => [ + 'class' => 'navbar-inverse navbar-fixed-top', + ], + ]); + $menuItems = [ + ['label' => 'Home', 'url' => ['/site/index']], + ]; + if (Yii::$app->user->isGuest) { + $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; + } else { + $menuItems[] = [ + 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', + 'url' => ['/site/logout'], + 'linkOptions' => ['data-method' => 'post'] + ]; + } + echo Nav::widget([ + 'options' => ['class' => 'navbar-nav navbar-right'], + 'items' => $menuItems, + ]); + NavBar::end(); + ?> -
- isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], - ]) ?> - -
-
+
+ isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], + ]) ?> + +
+
- + - endBody() ?> + endBody() ?> endPage() ?> diff --git a/apps/advanced/backend/views/site/error.php b/apps/advanced/backend/views/site/error.php index 1b7ce0422e6..c172fd621a3 100644 --- a/apps/advanced/backend/views/site/error.php +++ b/apps/advanced/backend/views/site/error.php @@ -13,17 +13,17 @@ ?>
-

title) ?>

+

title) ?>

-
- -
+
+ +
-

- The above error occurred while the Web server was processing your request. -

-

- Please contact us if you think this is a server error. Thank you. -

+

+ The above error occurred while the Web server was processing your request. +

+

+ Please contact us if you think this is a server error. Thank you. +

diff --git a/apps/advanced/backend/views/site/index.php b/apps/advanced/backend/views/site/index.php index bcb22781a60..6b6394e5626 100644 --- a/apps/advanced/backend/views/site/index.php +++ b/apps/advanced/backend/views/site/index.php @@ -6,48 +6,48 @@ ?>
-
-

Congratulations!

+
+

Congratulations!

-

You have successfully created your Yii-powered application.

+

You have successfully created your Yii-powered application.

-

Get started with Yii

-
+

Get started with Yii

+
-
+
-
-
-

Heading

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Documentation »

-
-
-

Heading

+

Yii Documentation »

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Forum »

-
-
-

Heading

+

Yii Forum »

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Extensions »

-
-
+

Yii Extensions »

+
+
-
+
diff --git a/apps/advanced/backend/views/site/login.php b/apps/advanced/backend/views/site/login.php index 9b0ff7df79e..328c386f513 100644 --- a/apps/advanced/backend/views/site/login.php +++ b/apps/advanced/backend/views/site/login.php @@ -11,20 +11,20 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

Please fill out the following fields to login:

+

Please fill out the following fields to login:

-
-
- 'login-form']); ?> - field($model, 'username') ?> - field($model, 'password')->passwordInput() ?> - field($model, 'rememberMe')->checkbox() ?> -
- 'btn btn-primary', 'name' => 'login-button']) ?> -
- -
-
+
+
+ 'login-form']); ?> + field($model, 'username') ?> + field($model, 'password')->passwordInput() ?> + field($model, 'rememberMe')->checkbox() ?> +
+ 'btn btn-primary', 'name' => 'login-button']) ?> +
+ +
+
diff --git a/apps/advanced/common/config/main.php b/apps/advanced/common/config/main.php index c29fdb3e56a..67d498292d8 100644 --- a/apps/advanced/common/config/main.php +++ b/apps/advanced/common/config/main.php @@ -1,10 +1,10 @@ dirname(dirname(__DIR__)) . '/vendor', - 'extensions' => require(__DIR__ . '/../../vendor/yiisoft/extensions.php'), - 'components' => [ - 'cache' => [ - 'class' => 'yii\caching\FileCache', - ], - ], + 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', + 'extensions' => require(__DIR__ . '/../../vendor/yiisoft/extensions.php'), + 'components' => [ + 'cache' => [ + 'class' => 'yii\caching\FileCache', + ], + ], ]; diff --git a/apps/advanced/common/config/params.php b/apps/advanced/common/config/params.php index fbc7c5653b0..4ec9ba6d00d 100644 --- a/apps/advanced/common/config/params.php +++ b/apps/advanced/common/config/params.php @@ -1,6 +1,6 @@ 'admin@example.com', - 'supportEmail' => 'support@example.com', - 'user.passwordResetTokenExpire' => 3600, + 'adminEmail' => 'admin@example.com', + 'supportEmail' => 'support@example.com', + 'user.passwordResetTokenExpire' => 3600, ]; diff --git a/apps/advanced/common/mail/layouts/html.php b/apps/advanced/common/mail/layouts/html.php index 8e2707dc5d1..a9689cc2e5b 100644 --- a/apps/advanced/common/mail/layouts/html.php +++ b/apps/advanced/common/mail/layouts/html.php @@ -10,14 +10,14 @@ - - <?= Html::encode($this->title) ?> - head() ?> + + <?= Html::encode($this->title) ?> + head() ?> - beginBody() ?> - - endBody() ?> + beginBody() ?> + + endBody() ?> -endPage() ?> \ No newline at end of file +endPage() ?> diff --git a/apps/advanced/common/mail/passwordResetToken.php b/apps/advanced/common/mail/passwordResetToken.php index b8f8d870799..5135647d705 100644 --- a/apps/advanced/common/mail/passwordResetToken.php +++ b/apps/advanced/common/mail/passwordResetToken.php @@ -13,4 +13,4 @@ Follow the link below to reset your password: - +hasErrors()) { - $user = $this->getUser(); - if (!$user || !$user->validatePassword($this->password)) { - $this->addError('password', 'Incorrect username or password.'); - } - } - } + /** + * Validates the password. + * This method serves as the inline validation for password. + */ + public function validatePassword() + { + if (!$this->hasErrors()) { + $user = $this->getUser(); + if (!$user || !$user->validatePassword($this->password)) { + $this->addError('password', 'Incorrect username or password.'); + } + } + } - /** - * Logs in a user using the provided username and password. - * - * @return boolean whether the user is logged in successfully - */ - public function login() - { - if ($this->validate()) { - return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); - } else { - return false; - } - } + /** + * Logs in a user using the provided username and password. + * + * @return boolean whether the user is logged in successfully + */ + public function login() + { + if ($this->validate()) { + return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0); + } else { + return false; + } + } - /** - * Finds user by [[username]] - * - * @return User|null - */ - public function getUser() - { - if ($this->_user === false) { - $this->_user = User::findByUsername($this->username); - } - return $this->_user; - } + /** + * Finds user by [[username]] + * + * @return User|null + */ + public function getUser() + { + if ($this->_user === false) { + $this->_user = User::findByUsername($this->username); + } + + return $this->_user; + } } diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php index f3b39cc11a3..9cac5329b83 100644 --- a/apps/advanced/common/models/User.php +++ b/apps/advanced/common/models/User.php @@ -23,186 +23,186 @@ */ class User extends ActiveRecord implements IdentityInterface { - const STATUS_DELETED = 0; - const STATUS_ACTIVE = 10; - - const ROLE_USER = 10; - - /** - * Creates a new user - * - * @param array $attributes the attributes given by field => value - * @return static|null the newly created model, or null on failure - */ - public static function create($attributes) - { - /** @var User $user */ - $user = new static(); - $user->setAttributes($attributes); - $user->setPassword($attributes['password']); - $user->generateAuthKey(); - if ($user->save()) { - return $user; - } else { - return null; - } - } - - /** - * @inheritdoc - */ - public function behaviors() - { - return [ - 'timestamp' => [ - 'class' => 'yii\behaviors\TimestampBehavior', - 'attributes' => [ - ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], - ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'], - ], - ], - ]; - } - - /** - * @inheritdoc - */ - public static function findIdentity($id) - { - return static::find($id); - } - - /** - * @inheritdoc - */ - public static function findIdentityByAccessToken($token) - { - throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); - } - - /** - * Finds user by username - * - * @param string $username - * @return static|null - */ - public static function findByUsername($username) - { - return static::find(['username' => $username, 'status' => self::STATUS_ACTIVE]); - } - - /** - * Finds user by password reset token - * - * @param string $token password reset token - * @return static|null - */ - public static function findByPasswordResetToken($token) - { - $expire = \Yii::$app->params['user.passwordResetTokenExpire']; - $parts = explode('_', $token); - $timestamp = (int)end($parts); - if ($timestamp + $expire < time()) { - // token expired - return null; - } - - return static::find([ - 'password_reset_token' => $token, - 'status' => self::STATUS_ACTIVE, - ]); - } - - /** - * @inheritdoc - */ - public function getId() - { - return $this->getPrimaryKey(); - } - - /** - * @inheritdoc - */ - public function getAuthKey() - { - return $this->auth_key; - } - - /** - * @inheritdoc - */ - public function validateAuthKey($authKey) - { - return $this->getAuthKey() === $authKey; - } - - /** - * Validates password - * - * @param string $password password to validate - * @return boolean if password provided is valid for current user - */ - public function validatePassword($password) - { - return Security::validatePassword($password, $this->password_hash); - } - - /** - * Generates password hash from password and sets it to the model - * - * @param string $password - */ - public function setPassword($password) - { - $this->password_hash = Security::generatePasswordHash($password); - } - - /** - * Generates "remember me" authentication key - */ - public function generateAuthKey() - { - $this->auth_key = Security::generateRandomKey(); - } - - /** - * Generates new password reset token - */ - public function generatePasswordResetToken() - { - $this->password_reset_token = Security::generateRandomKey() . '_' . time(); - } - - /** - * Removes password reset token - */ - public function removePasswordResetToken() - { - $this->password_reset_token = null; - } - - /** - * @inheritdoc - */ - public function rules() - { - return [ - ['status', 'default', 'value' => self::STATUS_ACTIVE], - ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]], - - ['role', 'default', 'value' => self::ROLE_USER], - ['role', 'in', 'range' => [self::ROLE_USER]], - - ['username', 'filter', 'filter' => 'trim'], - ['username', 'required'], - ['username', 'unique'], - ['username', 'string', 'min' => 2, 'max' => 255], - - ['email', 'filter', 'filter' => 'trim'], - ['email', 'required'], - ['email', 'email'], - ['email', 'unique'], - ]; - } + const STATUS_DELETED = 0; + const STATUS_ACTIVE = 10; + + const ROLE_USER = 10; + + /** + * Creates a new user + * + * @param array $attributes the attributes given by field => value + * @return static|null the newly created model, or null on failure + */ + public static function create($attributes) + { + /** @var User $user */ + $user = new static(); + $user->setAttributes($attributes); + $user->setPassword($attributes['password']); + $user->generateAuthKey(); + if ($user->save()) { + return $user; + } else { + return null; + } + } + + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + 'timestamp' => [ + 'class' => 'yii\behaviors\TimestampBehavior', + 'attributes' => [ + ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], + ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + public static function findIdentity($id) + { + return static::find($id); + } + + /** + * @inheritdoc + */ + public static function findIdentityByAccessToken($token) + { + throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); + } + + /** + * Finds user by username + * + * @param string $username + * @return static|null + */ + public static function findByUsername($username) + { + return static::find(['username' => $username, 'status' => self::STATUS_ACTIVE]); + } + + /** + * Finds user by password reset token + * + * @param string $token password reset token + * @return static|null + */ + public static function findByPasswordResetToken($token) + { + $expire = \Yii::$app->params['user.passwordResetTokenExpire']; + $parts = explode('_', $token); + $timestamp = (int) end($parts); + if ($timestamp + $expire < time()) { + // token expired + return null; + } + + return static::find([ + 'password_reset_token' => $token, + 'status' => self::STATUS_ACTIVE, + ]); + } + + /** + * @inheritdoc + */ + public function getId() + { + return $this->getPrimaryKey(); + } + + /** + * @inheritdoc + */ + public function getAuthKey() + { + return $this->auth_key; + } + + /** + * @inheritdoc + */ + public function validateAuthKey($authKey) + { + return $this->getAuthKey() === $authKey; + } + + /** + * Validates password + * + * @param string $password password to validate + * @return boolean if password provided is valid for current user + */ + public function validatePassword($password) + { + return Security::validatePassword($password, $this->password_hash); + } + + /** + * Generates password hash from password and sets it to the model + * + * @param string $password + */ + public function setPassword($password) + { + $this->password_hash = Security::generatePasswordHash($password); + } + + /** + * Generates "remember me" authentication key + */ + public function generateAuthKey() + { + $this->auth_key = Security::generateRandomKey(); + } + + /** + * Generates new password reset token + */ + public function generatePasswordResetToken() + { + $this->password_reset_token = Security::generateRandomKey() . '_' . time(); + } + + /** + * Removes password reset token + */ + public function removePasswordResetToken() + { + $this->password_reset_token = null; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['status', 'default', 'value' => self::STATUS_ACTIVE], + ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]], + + ['role', 'default', 'value' => self::ROLE_USER], + ['role', 'in', 'range' => [self::ROLE_USER]], + + ['username', 'filter', 'filter' => 'trim'], + ['username', 'required'], + ['username', 'unique'], + ['username', 'string', 'min' => 2, 'max' => 255], + + ['email', 'filter', 'filter' => 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'unique'], + ]; + } } diff --git a/apps/advanced/common/tests/_config.php b/apps/advanced/common/tests/_config.php index d9cc356dd80..24ec1b56aec 100644 --- a/apps/advanced/common/tests/_config.php +++ b/apps/advanced/common/tests/_config.php @@ -3,12 +3,12 @@ * application configurations shared by all test types */ return [ - 'components' => [ - 'mail' => [ - 'useFileTransport' => true, - ], - 'urlManager' => [ - 'showScriptName' => true, - ], - ], + 'components' => [ + 'mail' => [ + 'useFileTransport' => true, + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], ]; diff --git a/apps/advanced/common/tests/_helpers/CodeHelper.php b/apps/advanced/common/tests/_helpers/CodeHelper.php index 972c8f3f1ad..eea532c3b7b 100644 --- a/apps/advanced/common/tests/_helpers/CodeHelper.php +++ b/apps/advanced/common/tests/_helpers/CodeHelper.php @@ -1,7 +1,7 @@ loadFixtures(); - } + /** + * Method called before any suite tests run. Loads User fixture login user + * to use in acceptance and functional tests. + * @param array $settings + */ + public function _beforeSuite($settings = []) + { + $this->loadFixtures(); + } - /** - * Method is called after all suite tests run - */ - public function _afterSuite() - { - $this->unloadFixtures(); - } + /** + * Method is called after all suite tests run + */ + public function _afterSuite() + { + $this->unloadFixtures(); + } - protected function fixtures() - { - return [ - 'user' => [ - 'class' => UserFixture::className(), - 'dataFile' => '@common/tests/fixtures/data/init_login.php', - ], - ]; - } + protected function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@common/tests/fixtures/data/init_login.php', + ], + ]; + } } diff --git a/apps/advanced/common/tests/_helpers/TestHelper.php b/apps/advanced/common/tests/_helpers/TestHelper.php index 37737cd6ed6..826e0159688 100644 --- a/apps/advanced/common/tests/_helpers/TestHelper.php +++ b/apps/advanced/common/tests/_helpers/TestHelper.php @@ -1,7 +1,7 @@ guy->fillField('input[name="LoginForm[username]"]', $username); - $this->guy->fillField('input[name="LoginForm[password]"]', $password); - $this->guy->click('login-button'); - } + /** + * @param string $username + * @param string $password + */ + public function login($username, $password) + { + $this->guy->fillField('input[name="LoginForm[username]"]', $username); + $this->guy->fillField('input[name="LoginForm[password]"]', $password); + $this->guy->click('login-button'); + } } diff --git a/apps/advanced/common/tests/fixtures/UserFixture.php b/apps/advanced/common/tests/fixtures/UserFixture.php index 6d69cfe0807..4aea8fd92d8 100644 --- a/apps/advanced/common/tests/fixtures/UserFixture.php +++ b/apps/advanced/common/tests/fixtures/UserFixture.php @@ -6,5 +6,5 @@ class UserFixture extends ActiveFixture { - public $modelClass = 'common\models\User'; + public $modelClass = 'common\models\User'; } diff --git a/apps/advanced/common/tests/fixtures/data/init_login.php b/apps/advanced/common/tests/fixtures/data/init_login.php index 7b4bbbe09d6..841e2794853 100644 --- a/apps/advanced/common/tests/fixtures/data/init_login.php +++ b/apps/advanced/common/tests/fixtures/data/init_login.php @@ -1,14 +1,14 @@ 'erau', - 'auth_key' => 'tUu1qHcde0diwUol3xeI-18MuHkkprQI', - // password_0 - 'password_hash' => '$2y$13$nJ1WDlBaGcbCdbNC5.5l4.sgy.OMEKCqtDQOdQ2OWpgiKRWYyzzne', - 'password_reset_token' => 'RkD_Jw0_8HEedzLk7MM-ZKEFfYR7VbMr_1392559490', - 'created_at' => '1392559490', - 'updated_at' => '1392559490', - 'email' => 'sfriesen@jenkins.info', - ], + [ + 'username' => 'erau', + 'auth_key' => 'tUu1qHcde0diwUol3xeI-18MuHkkprQI', + // password_0 + 'password_hash' => '$2y$13$nJ1WDlBaGcbCdbNC5.5l4.sgy.OMEKCqtDQOdQ2OWpgiKRWYyzzne', + 'password_reset_token' => 'RkD_Jw0_8HEedzLk7MM-ZKEFfYR7VbMr_1392559490', + 'created_at' => '1392559490', + 'updated_at' => '1392559490', + 'email' => 'sfriesen@jenkins.info', + ], ]; diff --git a/apps/advanced/common/tests/templates/fixtures/tbl_user.php b/apps/advanced/common/tests/templates/fixtures/tbl_user.php index 4d14b2d40e3..29b0558d2b9 100644 --- a/apps/advanced/common/tests/templates/fixtures/tbl_user.php +++ b/apps/advanced/common/tests/templates/fixtures/tbl_user.php @@ -3,26 +3,31 @@ use yii\helpers\Security; return [ - 'username' => 'userName', - 'auth_key' => function ($fixture, $faker, $index) { - $fixture['auth_key'] = Security::generateRandomKey(); - return $fixture; - }, - 'password_hash' => function ($fixture, $faker, $index) { - $fixture['password_hash'] = Security::generatePasswordHash('password_' . $index); - return $fixture; - }, - 'password_reset_token' => function ($fixture, $faker, $index) { - $fixture['password_reset_token'] = Security::generateRandomKey() . '_' . time(); - return $fixture; - }, - 'created_at' => function ($fixture, $faker, $index) { - $fixture['created_at'] = time(); - return $fixture; - }, - 'updated_at' => function ($fixture, $faker, $index) { - $fixture['updated_at'] = time(); - return $fixture; - }, - 'email' => 'email', + 'username' => 'userName', + 'auth_key' => function ($fixture, $faker, $index) { + $fixture['auth_key'] = Security::generateRandomKey(); + + return $fixture; + }, + 'password_hash' => function ($fixture, $faker, $index) { + $fixture['password_hash'] = Security::generatePasswordHash('password_' . $index); + + return $fixture; + }, + 'password_reset_token' => function ($fixture, $faker, $index) { + $fixture['password_reset_token'] = Security::generateRandomKey() . '_' . time(); + + return $fixture; + }, + 'created_at' => function ($fixture, $faker, $index) { + $fixture['created_at'] = time(); + + return $fixture; + }, + 'updated_at' => function ($fixture, $faker, $index) { + $fixture['updated_at'] = time(); + + return $fixture; + }, + 'email' => 'email', ]; diff --git a/apps/advanced/common/tests/unit/DbTestCase.php b/apps/advanced/common/tests/unit/DbTestCase.php index 9b5923ba7c4..364ede7e975 100644 --- a/apps/advanced/common/tests/unit/DbTestCase.php +++ b/apps/advanced/common/tests/unit/DbTestCase.php @@ -4,5 +4,5 @@ class DbTestCase extends \yii\codeception\DbTestCase { - public $appConfig = '@frontend/tests/unit/_config.php'; + public $appConfig = '@frontend/tests/unit/_config.php'; } diff --git a/apps/advanced/common/tests/unit/TestCase.php b/apps/advanced/common/tests/unit/TestCase.php index 11024deba23..e7919367d7d 100644 --- a/apps/advanced/common/tests/unit/TestCase.php +++ b/apps/advanced/common/tests/unit/TestCase.php @@ -4,5 +4,5 @@ class TestCase extends \yii\codeception\TestCase { - public $appConfig = '@common/tests/unit/_config.php'; + public $appConfig = '@common/tests/unit/_config.php'; } diff --git a/apps/advanced/common/tests/unit/_config.php b/apps/advanced/common/tests/unit/_config.php index 4628bf98248..d5092a76ba9 100644 --- a/apps/advanced/common/tests/unit/_config.php +++ b/apps/advanced/common/tests/unit/_config.php @@ -1,16 +1,16 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - 'id' => 'app-common', - 'basePath' => dirname(__DIR__), - ] + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + 'id' => 'app-common', + 'basePath' => dirname(__DIR__), + ] ); diff --git a/apps/advanced/common/tests/unit/_console.php b/apps/advanced/common/tests/unit/_console.php index 2c9aaff023a..dc61b7ce312 100644 --- a/apps/advanced/common/tests/unit/_console.php +++ b/apps/advanced/common/tests/unit/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/common/tests/unit/models/LoginFormTest.php b/apps/advanced/common/tests/unit/models/LoginFormTest.php index 9ca277d45b6..bd41bad1bd9 100644 --- a/apps/advanced/common/tests/unit/models/LoginFormTest.php +++ b/apps/advanced/common/tests/unit/models/LoginFormTest.php @@ -9,60 +9,61 @@ class LoginFormTest extends TestCase { - - use \Codeception\Specify; - - protected function tearDown() - { - Yii::$app->user->logout(); - parent::tearDown(); - } - - public function testLoginNoUser() - { - $model = $this->mockUser(null); - - $model->username = 'some_username'; - $model->password = 'some_password'; - - $this->specify('user should not be able to login, when there is no identity', function () use ($model) { - expect('model should not login user', $model->login())->false(); - expect('user should not be logged in', Yii::$app->user->isGuest)->true(); - }); - } - - public function testLoginWrongPassword() - { - $model = $this->mockUser(new User(['password_hash' => Security::generatePasswordHash('will-not-match')])); - - $model->username = 'demo'; - $model->password = 'wrong-password'; - - $this->specify('user should not be able to login with wrong password', function () use ($model) { - expect('model should not login user', $model->login())->false(); - expect('error message should be set', $model->errors)->hasKey('password'); - expect('user should not be logged in', Yii::$app->user->isGuest)->true(); - }); - } - - public function testLoginCorrect() - { - $model = $this->mockUser(new User(['password_hash' => Security::generatePasswordHash('demo')])); - - $model->username = 'demo'; - $model->password = 'demo'; - - $this->specify('user should be able to login with correct credentials', function () use ($model) { - expect('model should login user', $model->login())->true(); - expect('error message should not be set', $model->errors)->hasntKey('password'); - expect('user should be logged in', Yii::$app->user->isGuest)->false(); - }); - } - - private function mockUser($user) - { - $loginForm = $this->getMock('common\models\LoginForm', ['getUser']); - $loginForm->expects($this->any())->method('getUser')->will($this->returnValue($user)); - return $loginForm; - } + + use \Codeception\Specify; + + protected function tearDown() + { + Yii::$app->user->logout(); + parent::tearDown(); + } + + public function testLoginNoUser() + { + $model = $this->mockUser(null); + + $model->username = 'some_username'; + $model->password = 'some_password'; + + $this->specify('user should not be able to login, when there is no identity', function () use ($model) { + expect('model should not login user', $model->login())->false(); + expect('user should not be logged in', Yii::$app->user->isGuest)->true(); + }); + } + + public function testLoginWrongPassword() + { + $model = $this->mockUser(new User(['password_hash' => Security::generatePasswordHash('will-not-match')])); + + $model->username = 'demo'; + $model->password = 'wrong-password'; + + $this->specify('user should not be able to login with wrong password', function () use ($model) { + expect('model should not login user', $model->login())->false(); + expect('error message should be set', $model->errors)->hasKey('password'); + expect('user should not be logged in', Yii::$app->user->isGuest)->true(); + }); + } + + public function testLoginCorrect() + { + $model = $this->mockUser(new User(['password_hash' => Security::generatePasswordHash('demo')])); + + $model->username = 'demo'; + $model->password = 'demo'; + + $this->specify('user should be able to login with correct credentials', function () use ($model) { + expect('model should login user', $model->login())->true(); + expect('error message should not be set', $model->errors)->hasntKey('password'); + expect('user should be logged in', Yii::$app->user->isGuest)->false(); + }); + } + + private function mockUser($user) + { + $loginForm = $this->getMock('common\models\LoginForm', ['getUser']); + $loginForm->expects($this->any())->method('getUser')->will($this->returnValue($user)); + + return $loginForm; + } } diff --git a/apps/advanced/console/config/main.php b/apps/advanced/console/config/main.php index 97b639ddea3..f91278d2627 100644 --- a/apps/advanced/console/config/main.php +++ b/apps/advanced/console/config/main.php @@ -1,25 +1,25 @@ 'app-console', - 'basePath' => dirname(__DIR__), - 'controllerNamespace' => 'console\controllers', - 'modules' => [], - 'components' => [ - 'log' => [ - 'targets' => [ - [ - 'class' => 'yii\log\FileTarget', - 'levels' => ['error', 'warning'], - ], - ], - ], - ], - 'params' => $params, + 'id' => 'app-console', + 'basePath' => dirname(__DIR__), + 'controllerNamespace' => 'console\controllers', + 'modules' => [], + 'components' => [ + 'log' => [ + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + ], + 'params' => $params, ]; diff --git a/apps/advanced/console/config/params.php b/apps/advanced/console/config/params.php index 0e625dc7d18..7f754b91fe0 100644 --- a/apps/advanced/console/config/params.php +++ b/apps/advanced/console/config/params.php @@ -1,4 +1,4 @@ 'admin@example.com', + 'adminEmail' => 'admin@example.com', ]; diff --git a/apps/advanced/console/migrations/m130524_201442_init.php b/apps/advanced/console/migrations/m130524_201442_init.php index a4c5e1924fa..c5566b6e835 100644 --- a/apps/advanced/console/migrations/m130524_201442_init.php +++ b/apps/advanced/console/migrations/m130524_201442_init.php @@ -4,30 +4,30 @@ class m130524_201442_init extends \yii\db\Migration { - public function up() - { - $tableOptions = null; - if ($this->db->driverName === 'mysql') { - $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; - } + public function up() + { + $tableOptions = null; + if ($this->db->driverName === 'mysql') { + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; + } - $this->createTable('tbl_user', [ - 'id' => Schema::TYPE_PK, - 'username' => Schema::TYPE_STRING . ' NOT NULL', - 'auth_key' => Schema::TYPE_STRING . '(32) NOT NULL', - 'password_hash' => Schema::TYPE_STRING . ' NOT NULL', - 'password_reset_token' => Schema::TYPE_STRING, - 'email' => Schema::TYPE_STRING . ' NOT NULL', - 'role' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10', + $this->createTable('tbl_user', [ + 'id' => Schema::TYPE_PK, + 'username' => Schema::TYPE_STRING . ' NOT NULL', + 'auth_key' => Schema::TYPE_STRING . '(32) NOT NULL', + 'password_hash' => Schema::TYPE_STRING . ' NOT NULL', + 'password_reset_token' => Schema::TYPE_STRING, + 'email' => Schema::TYPE_STRING . ' NOT NULL', + 'role' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10', - 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10', - 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', - 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', - ], $tableOptions); - } + 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10', + 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', + 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + } - public function down() - { - $this->dropTable('tbl_user'); - } + public function down() + { + $this->dropTable('tbl_user'); + } } diff --git a/apps/advanced/console/tests/_config.php b/apps/advanced/console/tests/_config.php index d9cc356dd80..24ec1b56aec 100644 --- a/apps/advanced/console/tests/_config.php +++ b/apps/advanced/console/tests/_config.php @@ -3,12 +3,12 @@ * application configurations shared by all test types */ return [ - 'components' => [ - 'mail' => [ - 'useFileTransport' => true, - ], - 'urlManager' => [ - 'showScriptName' => true, - ], - ], + 'components' => [ + 'mail' => [ + 'useFileTransport' => true, + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], ]; diff --git a/apps/advanced/console/tests/_helpers/CodeHelper.php b/apps/advanced/console/tests/_helpers/CodeHelper.php index 972c8f3f1ad..eea532c3b7b 100644 --- a/apps/advanced/console/tests/_helpers/CodeHelper.php +++ b/apps/advanced/console/tests/_helpers/CodeHelper.php @@ -1,7 +1,7 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/console/tests/unit/_console.php b/apps/advanced/console/tests/unit/_console.php index 280dfc002bd..5eff4df6f08 100644 --- a/apps/advanced/console/tests/unit/_console.php +++ b/apps/advanced/console/tests/unit/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/environments/dev/backend/config/main-local.php b/apps/advanced/environments/dev/backend/config/main-local.php index 6823fd95bc1..c13bb3e9b7f 100644 --- a/apps/advanced/environments/dev/backend/config/main-local.php +++ b/apps/advanced/environments/dev/backend/config/main-local.php @@ -3,10 +3,10 @@ $config = []; if (!YII_ENV_TEST) { - // configuration adjustments for 'dev' environment - $config['preload'][] = 'debug'; - $config['modules']['debug'] = 'yii\debug\Module'; - $config['modules']['gii'] = 'yii\gii\Module'; + // configuration adjustments for 'dev' environment + $config['preload'][] = 'debug'; + $config['modules']['debug'] = 'yii\debug\Module'; + $config['modules']['gii'] = 'yii\gii\Module'; } return $config; diff --git a/apps/advanced/environments/dev/backend/web/index-test.php b/apps/advanced/environments/dev/backend/web/index-test.php index 1cd07941dc1..65d2e77be2b 100644 --- a/apps/advanced/environments/dev/backend/web/index-test.php +++ b/apps/advanced/environments/dev/backend/web/index-test.php @@ -2,7 +2,7 @@ // NOTE: Make sure this file is not accessible when deployed to production if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { - die('You are not allowed to access this file.'); + die('You are not allowed to access this file.'); } defined('YII_DEBUG') or define('YII_DEBUG', true); diff --git a/apps/advanced/environments/dev/backend/web/index.php b/apps/advanced/environments/dev/backend/web/index.php index ed8e69f8b99..b4748ba7e28 100644 --- a/apps/advanced/environments/dev/backend/web/index.php +++ b/apps/advanced/environments/dev/backend/web/index.php @@ -7,10 +7,10 @@ require(__DIR__ . '/../../common/config/aliases.php'); $config = yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../common/config/main.php'), - require(__DIR__ . '/../../common/config/main-local.php'), - require(__DIR__ . '/../config/main.php'), - require(__DIR__ . '/../config/main-local.php') + require(__DIR__ . '/../../common/config/main.php'), + require(__DIR__ . '/../../common/config/main-local.php'), + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') ); $application = new yii\web\Application($config); diff --git a/apps/advanced/environments/dev/common/config/main-local.php b/apps/advanced/environments/dev/common/config/main-local.php index d72af1878b7..dc1233a07c7 100644 --- a/apps/advanced/environments/dev/common/config/main-local.php +++ b/apps/advanced/environments/dev/common/config/main-local.php @@ -1,17 +1,17 @@ [ - 'db' => [ - 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8', - ], - 'mail' => [ - 'class' => 'yii\swiftmailer\Mailer', - 'viewPath' => '@common/mail', - 'useFileTransport' => true, - ], - ], + 'components' => [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ], + 'mail' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'viewPath' => '@common/mail', + 'useFileTransport' => true, + ], + ], ]; diff --git a/apps/advanced/environments/dev/frontend/config/main-local.php b/apps/advanced/environments/dev/frontend/config/main-local.php index 6823fd95bc1..c13bb3e9b7f 100644 --- a/apps/advanced/environments/dev/frontend/config/main-local.php +++ b/apps/advanced/environments/dev/frontend/config/main-local.php @@ -3,10 +3,10 @@ $config = []; if (!YII_ENV_TEST) { - // configuration adjustments for 'dev' environment - $config['preload'][] = 'debug'; - $config['modules']['debug'] = 'yii\debug\Module'; - $config['modules']['gii'] = 'yii\gii\Module'; + // configuration adjustments for 'dev' environment + $config['preload'][] = 'debug'; + $config['modules']['debug'] = 'yii\debug\Module'; + $config['modules']['gii'] = 'yii\gii\Module'; } return $config; diff --git a/apps/advanced/environments/dev/frontend/web/index-test.php b/apps/advanced/environments/dev/frontend/web/index-test.php index 1cd07941dc1..65d2e77be2b 100644 --- a/apps/advanced/environments/dev/frontend/web/index-test.php +++ b/apps/advanced/environments/dev/frontend/web/index-test.php @@ -2,7 +2,7 @@ // NOTE: Make sure this file is not accessible when deployed to production if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { - die('You are not allowed to access this file.'); + die('You are not allowed to access this file.'); } defined('YII_DEBUG') or define('YII_DEBUG', true); diff --git a/apps/advanced/environments/dev/frontend/web/index.php b/apps/advanced/environments/dev/frontend/web/index.php index ed8e69f8b99..b4748ba7e28 100644 --- a/apps/advanced/environments/dev/frontend/web/index.php +++ b/apps/advanced/environments/dev/frontend/web/index.php @@ -7,10 +7,10 @@ require(__DIR__ . '/../../common/config/aliases.php'); $config = yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../common/config/main.php'), - require(__DIR__ . '/../../common/config/main-local.php'), - require(__DIR__ . '/../config/main.php'), - require(__DIR__ . '/../config/main-local.php') + require(__DIR__ . '/../../common/config/main.php'), + require(__DIR__ . '/../../common/config/main-local.php'), + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') ); $application = new yii\web\Application($config); diff --git a/apps/advanced/environments/index.php b/apps/advanced/environments/index.php index a2097bf6e66..c2f032df84e 100644 --- a/apps/advanced/environments/index.php +++ b/apps/advanced/environments/index.php @@ -17,22 +17,22 @@ * ``` */ return [ - 'Development' => [ - 'path' => 'dev', - 'writable' => [ - // handled by composer.json already - ], - 'executable' => [ - 'yii', - ], - ], - 'Production' => [ - 'path' => 'prod', - 'writable' => [ - // handled by composer.json already - ], - 'executable' => [ - 'yii', - ], - ], + 'Development' => [ + 'path' => 'dev', + 'writable' => [ + // handled by composer.json already + ], + 'executable' => [ + 'yii', + ], + ], + 'Production' => [ + 'path' => 'prod', + 'writable' => [ + // handled by composer.json already + ], + 'executable' => [ + 'yii', + ], + ], ]; diff --git a/apps/advanced/environments/prod/backend/web/index.php b/apps/advanced/environments/prod/backend/web/index.php index 8a215cea6ce..72177479052 100644 --- a/apps/advanced/environments/prod/backend/web/index.php +++ b/apps/advanced/environments/prod/backend/web/index.php @@ -7,10 +7,10 @@ require(__DIR__ . '/../../common/config/aliases.php'); $config = yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../common/config/main.php'), - require(__DIR__ . '/../../common/config/main-local.php'), - require(__DIR__ . '/../config/main.php'), - require(__DIR__ . '/../config/main-local.php') + require(__DIR__ . '/../../common/config/main.php'), + require(__DIR__ . '/../../common/config/main-local.php'), + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') ); $application = new yii\web\Application($config); diff --git a/apps/advanced/environments/prod/common/config/main-local.php b/apps/advanced/environments/prod/common/config/main-local.php index e20c7d9b534..0d5eed3eb92 100644 --- a/apps/advanced/environments/prod/common/config/main-local.php +++ b/apps/advanced/environments/prod/common/config/main-local.php @@ -1,16 +1,16 @@ [ - 'db' => [ - 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8', - ], - 'mail' => [ - 'class' => 'yii\swiftmailer\Mailer', - 'viewPath' => '@common/mail', - ], - ], + 'components' => [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=yii2advanced', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ], + 'mail' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'viewPath' => '@common/mail', + ], + ], ]; diff --git a/apps/advanced/environments/prod/frontend/web/index.php b/apps/advanced/environments/prod/frontend/web/index.php index 8a215cea6ce..72177479052 100644 --- a/apps/advanced/environments/prod/frontend/web/index.php +++ b/apps/advanced/environments/prod/frontend/web/index.php @@ -7,10 +7,10 @@ require(__DIR__ . '/../../common/config/aliases.php'); $config = yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../common/config/main.php'), - require(__DIR__ . '/../../common/config/main-local.php'), - require(__DIR__ . '/../config/main.php'), - require(__DIR__ . '/../config/main-local.php') + require(__DIR__ . '/../../common/config/main.php'), + require(__DIR__ . '/../../common/config/main-local.php'), + require(__DIR__ . '/../config/main.php'), + require(__DIR__ . '/../config/main-local.php') ); $application = new yii\web\Application($config); diff --git a/apps/advanced/frontend/assets/AppAsset.php b/apps/advanced/frontend/assets/AppAsset.php index 03c5382e601..995e3dc301b 100644 --- a/apps/advanced/frontend/assets/AppAsset.php +++ b/apps/advanced/frontend/assets/AppAsset.php @@ -15,15 +15,15 @@ */ class AppAsset extends AssetBundle { - public $basePath = '@webroot'; - public $baseUrl = '@web'; - public $css = [ - 'css/site.css', - ]; - public $js = [ - ]; - public $depends = [ - 'yii\web\YiiAsset', - 'yii\bootstrap\BootstrapAsset', - ]; + public $basePath = '@webroot'; + public $baseUrl = '@web'; + public $css = [ + 'css/site.css', + ]; + public $js = [ + ]; + public $depends = [ + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ]; } diff --git a/apps/advanced/frontend/config/main.php b/apps/advanced/frontend/config/main.php index 1c007355d41..e8918855518 100644 --- a/apps/advanced/frontend/config/main.php +++ b/apps/advanced/frontend/config/main.php @@ -1,32 +1,32 @@ 'app-frontend', - 'basePath' => dirname(__DIR__), - 'controllerNamespace' => 'frontend\controllers', - 'components' => [ - 'user' => [ - 'identityClass' => 'common\models\User', - 'enableAutoLogin' => true, - ], - 'log' => [ - 'traceLevel' => YII_DEBUG ? 3 : 0, - 'targets' => [ - [ - 'class' => 'yii\log\FileTarget', - 'levels' => ['error', 'warning'], - ], - ], - ], - 'errorHandler' => [ - 'errorAction' => 'site/error', - ], - ], - 'params' => $params, + 'id' => 'app-frontend', + 'basePath' => dirname(__DIR__), + 'controllerNamespace' => 'frontend\controllers', + 'components' => [ + 'user' => [ + 'identityClass' => 'common\models\User', + 'enableAutoLogin' => true, + ], + 'log' => [ + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + 'errorHandler' => [ + 'errorAction' => 'site/error', + ], + ], + 'params' => $params, ]; diff --git a/apps/advanced/frontend/config/params.php b/apps/advanced/frontend/config/params.php index 0e625dc7d18..7f754b91fe0 100644 --- a/apps/advanced/frontend/config/params.php +++ b/apps/advanced/frontend/config/params.php @@ -1,4 +1,4 @@ 'admin@example.com', + 'adminEmail' => 'admin@example.com', ]; diff --git a/apps/advanced/frontend/controllers/SiteController.php b/apps/advanced/frontend/controllers/SiteController.php index a45c8375871..5f58b584a29 100644 --- a/apps/advanced/frontend/controllers/SiteController.php +++ b/apps/advanced/frontend/controllers/SiteController.php @@ -17,151 +17,155 @@ */ class SiteController extends Controller { - /** - * @inheritdoc - */ - public function behaviors() - { - return [ - 'access' => [ - 'class' => \yii\web\AccessControl::className(), - 'only' => ['logout', 'signup'], - 'rules' => [ - [ - 'actions' => ['signup'], - 'allow' => true, - 'roles' => ['?'], - ], - [ - 'actions' => ['logout'], - 'allow' => true, - 'roles' => ['@'], - ], - ], - ], - 'verbs' => [ - 'class' => VerbFilter::className(), - 'actions' => [ - 'logout' => ['post'], - ], - ], - ]; - } - - /** - * @inheritdoc - */ - public function actions() - { - return [ - 'error' => [ - 'class' => 'yii\web\ErrorAction', - ], - 'captcha' => [ - 'class' => 'yii\captcha\CaptchaAction', - 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, - ], - ]; - } - - public function actionIndex() - { - return $this->render('index'); - } - - public function actionLogin() - { - if (!\Yii::$app->user->isGuest) { - return $this->goHome(); - } - - $model = new LoginForm(); - if ($model->load(Yii::$app->request->post()) && $model->login()) { - return $this->goBack(); - } else { - return $this->render('login', [ - 'model' => $model, - ]); - } - } - - public function actionLogout() - { - Yii::$app->user->logout(); - return $this->goHome(); - } - - public function actionContact() - { - $model = new ContactForm(); - if ($model->load(Yii::$app->request->post()) && $model->validate()) { - if ($model->sendEmail(Yii::$app->params['adminEmail'])) { - Yii::$app->session->setFlash('success', 'Thank you for contacting us. We will respond to you as soon as possible.'); - } else { - Yii::$app->session->setFlash('error', 'There was an error sending email.'); - } - return $this->refresh(); - } else { - return $this->render('contact', [ - 'model' => $model, - ]); - } - } - - public function actionAbout() - { - return $this->render('about'); - } - - public function actionSignup() - { - $model = new SignupForm(); - if ($model->load(Yii::$app->request->post())) { - $user = $model->signup(); - if ($user) { - if (Yii::$app->getUser()->login($user)) { - return $this->goHome(); - } - } - } - - return $this->render('signup', [ - 'model' => $model, - ]); - } - - public function actionRequestPasswordReset() - { - $model = new PasswordResetRequestForm(); - if ($model->load(Yii::$app->request->post()) && $model->validate()) { - if ($model->sendEmail()) { - Yii::$app->getSession()->setFlash('success', 'Check your email for further instructions.'); - return $this->goHome(); - } else { - Yii::$app->getSession()->setFlash('error', 'Sorry, we are unable to reset password for email provided.'); - } - } - - return $this->render('requestPasswordResetToken', [ - 'model' => $model, - ]); - } - - public function actionResetPassword($token) - { - try { - $model = new ResetPasswordForm($token); - } catch (InvalidParamException $e) { - throw new BadRequestHttpException($e->getMessage()); - } - - if ($model->load(Yii::$app->request->post()) && $model->validate() && $model->resetPassword()) { - Yii::$app->getSession()->setFlash('success', 'New password was saved.'); - return $this->goHome(); - } - - return $this->render('resetPassword', [ - 'model' => $model, - ]); - } + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + 'access' => [ + 'class' => \yii\web\AccessControl::className(), + 'only' => ['logout', 'signup'], + 'rules' => [ + [ + 'actions' => ['signup'], + 'allow' => true, + 'roles' => ['?'], + ], + [ + 'actions' => ['logout'], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'error' => [ + 'class' => 'yii\web\ErrorAction', + ], + 'captcha' => [ + 'class' => 'yii\captcha\CaptchaAction', + 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, + ], + ]; + } + + public function actionIndex() + { + return $this->render('index'); + } + + public function actionLogin() + { + if (!\Yii::$app->user->isGuest) { + return $this->goHome(); + } + + $model = new LoginForm(); + if ($model->load(Yii::$app->request->post()) && $model->login()) { + return $this->goBack(); + } else { + return $this->render('login', [ + 'model' => $model, + ]); + } + } + + public function actionLogout() + { + Yii::$app->user->logout(); + + return $this->goHome(); + } + + public function actionContact() + { + $model = new ContactForm(); + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + if ($model->sendEmail(Yii::$app->params['adminEmail'])) { + Yii::$app->session->setFlash('success', 'Thank you for contacting us. We will respond to you as soon as possible.'); + } else { + Yii::$app->session->setFlash('error', 'There was an error sending email.'); + } + + return $this->refresh(); + } else { + return $this->render('contact', [ + 'model' => $model, + ]); + } + } + + public function actionAbout() + { + return $this->render('about'); + } + + public function actionSignup() + { + $model = new SignupForm(); + if ($model->load(Yii::$app->request->post())) { + $user = $model->signup(); + if ($user) { + if (Yii::$app->getUser()->login($user)) { + return $this->goHome(); + } + } + } + + return $this->render('signup', [ + 'model' => $model, + ]); + } + + public function actionRequestPasswordReset() + { + $model = new PasswordResetRequestForm(); + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + if ($model->sendEmail()) { + Yii::$app->getSession()->setFlash('success', 'Check your email for further instructions.'); + + return $this->goHome(); + } else { + Yii::$app->getSession()->setFlash('error', 'Sorry, we are unable to reset password for email provided.'); + } + } + + return $this->render('requestPasswordResetToken', [ + 'model' => $model, + ]); + } + + public function actionResetPassword($token) + { + try { + $model = new ResetPasswordForm($token); + } catch (InvalidParamException $e) { + throw new BadRequestHttpException($e->getMessage()); + } + + if ($model->load(Yii::$app->request->post()) && $model->validate() && $model->resetPassword()) { + Yii::$app->getSession()->setFlash('success', 'New password was saved.'); + + return $this->goHome(); + } + + return $this->render('resetPassword', [ + 'model' => $model, + ]); + } } diff --git a/apps/advanced/frontend/models/ContactForm.php b/apps/advanced/frontend/models/ContactForm.php index 7d99ae869cc..1ad19ca7fe6 100644 --- a/apps/advanced/frontend/models/ContactForm.php +++ b/apps/advanced/frontend/models/ContactForm.php @@ -10,50 +10,50 @@ */ class ContactForm extends Model { - public $name; - public $email; - public $subject; - public $body; - public $verifyCode; + public $name; + public $email; + public $subject; + public $body; + public $verifyCode; - /** - * @inheritdoc - */ - public function rules() - { - return [ - // name, email, subject and body are required - [['name', 'email', 'subject', 'body'], 'required'], - // email has to be a valid email address - ['email', 'email'], - // verifyCode needs to be entered correctly - ['verifyCode', 'captcha'], - ]; - } + /** + * @inheritdoc + */ + public function rules() + { + return [ + // name, email, subject and body are required + [['name', 'email', 'subject', 'body'], 'required'], + // email has to be a valid email address + ['email', 'email'], + // verifyCode needs to be entered correctly + ['verifyCode', 'captcha'], + ]; + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'verifyCode' => 'Verification Code', - ]; - } + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'verifyCode' => 'Verification Code', + ]; + } - /** - * Sends an email to the specified email address using the information collected by this model. - * - * @param string $email the target email address - * @return boolean whether the email was sent - */ - public function sendEmail($email) - { - return Yii::$app->mail->compose() - ->setTo($email) - ->setFrom([$this->email => $this->name]) - ->setSubject($this->subject) - ->setTextBody($this->body) - ->send(); - } + /** + * Sends an email to the specified email address using the information collected by this model. + * + * @param string $email the target email address + * @return boolean whether the email was sent + */ + public function sendEmail($email) + { + return Yii::$app->mail->compose() + ->setTo($email) + ->setFrom([$this->email => $this->name]) + ->setSubject($this->subject) + ->setTextBody($this->body) + ->send(); + } } diff --git a/apps/advanced/frontend/models/PasswordResetRequestForm.php b/apps/advanced/frontend/models/PasswordResetRequestForm.php index 3b74be495ae..664bb049321 100644 --- a/apps/advanced/frontend/models/PasswordResetRequestForm.php +++ b/apps/advanced/frontend/models/PasswordResetRequestForm.php @@ -9,49 +9,49 @@ */ class PasswordResetRequestForm extends Model { - public $email; + public $email; - /** - * @inheritdoc - */ - public function rules() - { - return [ - ['email', 'filter', 'filter' => 'trim'], - ['email', 'required'], - ['email', 'email'], - ['email', 'exist', - 'targetClass' => '\common\models\User', - 'filter' => ['status' => User::STATUS_ACTIVE], - 'message' => 'There is no user with such email.' - ], - ]; - } + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['email', 'filter', 'filter' => 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'exist', + 'targetClass' => '\common\models\User', + 'filter' => ['status' => User::STATUS_ACTIVE], + 'message' => 'There is no user with such email.' + ], + ]; + } - /** - * Sends an email with a link, for resetting the password. - * - * @return boolean whether the email was send - */ - public function sendEmail() - { - /** @var User $user */ - $user = User::find([ - 'status' => User::STATUS_ACTIVE, - 'email' => $this->email, - ]); + /** + * Sends an email with a link, for resetting the password. + * + * @return boolean whether the email was send + */ + public function sendEmail() + { + /** @var User $user */ + $user = User::find([ + 'status' => User::STATUS_ACTIVE, + 'email' => $this->email, + ]); - if ($user) { - $user->generatePasswordResetToken(); - if ($user->save()) { - return \Yii::$app->mail->compose('passwordResetToken', ['user' => $user]) - ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot']) - ->setTo($this->email) - ->setSubject('Password reset for ' . \Yii::$app->name) - ->send(); - } - } + if ($user) { + $user->generatePasswordResetToken(); + if ($user->save()) { + return \Yii::$app->mail->compose('passwordResetToken', ['user' => $user]) + ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot']) + ->setTo($this->email) + ->setSubject('Password reset for ' . \Yii::$app->name) + ->send(); + } + } - return false; - } + return false; + } } diff --git a/apps/advanced/frontend/models/ResetPasswordForm.php b/apps/advanced/frontend/models/ResetPasswordForm.php index bef8b3fc4e0..93d1a69b2b8 100644 --- a/apps/advanced/frontend/models/ResetPasswordForm.php +++ b/apps/advanced/frontend/models/ResetPasswordForm.php @@ -11,53 +11,54 @@ */ class ResetPasswordForm extends Model { - public $password; + public $password; - /** - * @var \common\models\User - */ - private $_user; + /** + * @var \common\models\User + */ + private $_user; - /** - * Creates a form model given a token. - * - * @param string $token - * @param array $config name-value pairs that will be used to initialize the object properties - * @throws \yii\base\InvalidParamException if token is empty or not valid - */ - public function __construct($token, $config = []) - { - if (empty($token) || !is_string($token)) { - throw new InvalidParamException('Password reset token cannot be blank.'); - } - $this->_user = User::findByPasswordResetToken($token); - if (!$this->_user) { - throw new InvalidParamException('Wrong password reset token.'); - } - parent::__construct($config); - } + /** + * Creates a form model given a token. + * + * @param string $token + * @param array $config name-value pairs that will be used to initialize the object properties + * @throws \yii\base\InvalidParamException if token is empty or not valid + */ + public function __construct($token, $config = []) + { + if (empty($token) || !is_string($token)) { + throw new InvalidParamException('Password reset token cannot be blank.'); + } + $this->_user = User::findByPasswordResetToken($token); + if (!$this->_user) { + throw new InvalidParamException('Wrong password reset token.'); + } + parent::__construct($config); + } - /** - * @inheritdoc - */ - public function rules() - { - return [ - ['password', 'required'], - ['password', 'string', 'min' => 6], - ]; - } + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['password', 'required'], + ['password', 'string', 'min' => 6], + ]; + } - /** - * Resets password. - * - * @return boolean if password was reset. - */ - public function resetPassword() - { - $user = $this->_user; - $user->password = $this->password; - $user->removePasswordResetToken(); - return $user->save(); - } + /** + * Resets password. + * + * @return boolean if password was reset. + */ + public function resetPassword() + { + $user = $this->_user; + $user->password = $this->password; + $user->removePasswordResetToken(); + + return $user->save(); + } } diff --git a/apps/advanced/frontend/models/SignupForm.php b/apps/advanced/frontend/models/SignupForm.php index 03904246555..1d999b4f97f 100644 --- a/apps/advanced/frontend/models/SignupForm.php +++ b/apps/advanced/frontend/models/SignupForm.php @@ -10,41 +10,42 @@ */ class SignupForm extends Model { - public $username; - public $email; - public $password; + public $username; + public $email; + public $password; - /** - * @inheritdoc - */ - public function rules() - { - return [ - ['username', 'filter', 'filter' => 'trim'], - ['username', 'required'], - ['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'], - ['username', 'string', 'min' => 2, 'max' => 255], + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['username', 'filter', 'filter' => 'trim'], + ['username', 'required'], + ['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'], + ['username', 'string', 'min' => 2, 'max' => 255], - ['email', 'filter', 'filter' => 'trim'], - ['email', 'required'], - ['email', 'email'], - ['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken.'], + ['email', 'filter', 'filter' => 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken.'], - ['password', 'required'], - ['password', 'string', 'min' => 6], - ]; - } + ['password', 'required'], + ['password', 'string', 'min' => 6], + ]; + } - /** - * Signs user up. - * - * @return User|null the saved model or null if saving fails - */ - public function signup() - { - if ($this->validate()) { - return User::create($this->attributes); - } - return null; - } + /** + * Signs user up. + * + * @return User|null the saved model or null if saving fails + */ + public function signup() + { + if ($this->validate()) { + return User::create($this->attributes); + } + + return null; + } } diff --git a/apps/advanced/frontend/tests/_config.php b/apps/advanced/frontend/tests/_config.php index d9cc356dd80..24ec1b56aec 100644 --- a/apps/advanced/frontend/tests/_config.php +++ b/apps/advanced/frontend/tests/_config.php @@ -3,12 +3,12 @@ * application configurations shared by all test types */ return [ - 'components' => [ - 'mail' => [ - 'useFileTransport' => true, - ], - 'urlManager' => [ - 'showScriptName' => true, - ], - ], + 'components' => [ + 'mail' => [ + 'useFileTransport' => true, + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], ]; diff --git a/apps/advanced/frontend/tests/_helpers/CodeHelper.php b/apps/advanced/frontend/tests/_helpers/CodeHelper.php index 972c8f3f1ad..eea532c3b7b 100644 --- a/apps/advanced/frontend/tests/_helpers/CodeHelper.php +++ b/apps/advanced/frontend/tests/_helpers/CodeHelper.php @@ -1,7 +1,7 @@ $value) { - $inputType = $field === 'body' ? 'textarea' : 'input'; - $this->guy->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value); - } - $this->guy->click('contact-button'); - } + /** + * @param array $contactData + */ + public function submit(array $contactData) + { + foreach ($contactData as $field => $value) { + $inputType = $field === 'body' ? 'textarea' : 'input'; + $this->guy->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value); + } + $this->guy->click('contact-button'); + } } diff --git a/apps/advanced/frontend/tests/_pages/SignupPage.php b/apps/advanced/frontend/tests/_pages/SignupPage.php index 0281ac9a5c3..41bd153392c 100644 --- a/apps/advanced/frontend/tests/_pages/SignupPage.php +++ b/apps/advanced/frontend/tests/_pages/SignupPage.php @@ -7,17 +7,17 @@ class SignupPage extends BasePage { - public $route = 'site/signup'; + public $route = 'site/signup'; - /** - * @param array $signupData - */ - public function submit(array $signupData) - { - foreach ($signupData as $field => $value) { - $inputType = $field === 'body' ? 'textarea' : 'input'; - $this->guy->fillField($inputType . '[name="SignupForm[' . $field . ']"]', $value); - } - $this->guy->click('signup-button'); - } + /** + * @param array $signupData + */ + public function submit(array $signupData) + { + foreach ($signupData as $field => $value) { + $inputType = $field === 'body' ? 'textarea' : 'input'; + $this->guy->fillField($inputType . '[name="SignupForm[' . $field . ']"]', $value); + } + $this->guy->click('signup-button'); + } } diff --git a/apps/advanced/frontend/tests/acceptance.suite.yml b/apps/advanced/frontend/tests/acceptance.suite.yml index e6c64d16d5a..1eca29816c9 100644 --- a/apps/advanced/frontend/tests/acceptance.suite.yml +++ b/apps/advanced/frontend/tests/acceptance.suite.yml @@ -16,7 +16,7 @@ modules: - common\tests\_helpers\FixtureHelper # you can use WebDriver instead of PhpBrowser to test javascript and ajax. # This will require you to install selenium. See http://codeception.com/docs/04-AcceptanceTests#Selenium -# "restart" option is used by the WebDriver to start each time per test-file new session and cookies, +# "restart" option is used by the WebDriver to start each time per test-file new session and cookies, # it is useful if you want to login in your app in each test. # - WebDriver config: diff --git a/apps/advanced/frontend/tests/acceptance/ContactCept.php b/apps/advanced/frontend/tests/acceptance/ContactCept.php index 6351cb66f88..b8492cdc657 100644 --- a/apps/advanced/frontend/tests/acceptance/ContactCept.php +++ b/apps/advanced/frontend/tests/acceptance/ContactCept.php @@ -21,11 +21,11 @@ $I->amGoingTo('submit contact form with not correct email'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester.email', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester.email', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->expectTo('see that email adress is wrong'); $I->dontSee('Name cannot be blank', '.help-block'); @@ -36,10 +36,10 @@ $I->amGoingTo('submit contact form with correct data'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester@example.com', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester@example.com', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/advanced/frontend/tests/acceptance/SignupCest.php b/apps/advanced/frontend/tests/acceptance/SignupCest.php index 9de45d80e41..c086773ae1b 100644 --- a/apps/advanced/frontend/tests/acceptance/SignupCest.php +++ b/apps/advanced/frontend/tests/acceptance/SignupCest.php @@ -8,75 +8,75 @@ class SignupCest { - /** - * This method is called before each cest class test method - * @param \Codeception\Event\Test $event - */ - public function _before($event) - { - } + /** + * This method is called before each cest class test method + * @param \Codeception\Event\Test $event + */ + public function _before($event) + { + } - /** - * This method is called after each cest class test method, even if test failed. - * @param \Codeception\Event\Test $event - */ - public function _after($event) - { - User::deleteAll([ - 'email' => 'tester.email@example.com', - 'username' => 'tester', - ]); - } + /** + * This method is called after each cest class test method, even if test failed. + * @param \Codeception\Event\Test $event + */ + public function _after($event) + { + User::deleteAll([ + 'email' => 'tester.email@example.com', + 'username' => 'tester', + ]); + } - /** - * This method is called when test fails. - * @param \Codeception\Event\Fail $event - */ - public function _fail($event) - { - } + /** + * This method is called when test fails. + * @param \Codeception\Event\Fail $event + */ + public function _fail($event) + { + } - /** - * @param \WebGuy $I - * @param \Codeception\Scenario $scenario - */ - public function testUserSignup($I, $scenario) - { - $I->wantTo('ensure that signup works'); + /** + * @param \WebGuy $I + * @param \Codeception\Scenario $scenario + */ + public function testUserSignup($I, $scenario) + { + $I->wantTo('ensure that signup works'); - $signupPage = SignupPage::openBy($I); - $I->see('Signup', 'h1'); - $I->see('Please fill out the following fields to signup:'); + $signupPage = SignupPage::openBy($I); + $I->see('Signup', 'h1'); + $I->see('Please fill out the following fields to signup:'); - $I->amGoingTo('submit signup form with no data'); + $I->amGoingTo('submit signup form with no data'); - $signupPage->submit([]); + $signupPage->submit([]); - $I->expectTo('see validation errors'); - $I->see('Username cannot be blank.', '.help-block'); - $I->see('Email cannot be blank.', '.help-block'); - $I->see('Password cannot be blank.', '.help-block'); + $I->expectTo('see validation errors'); + $I->see('Username cannot be blank.', '.help-block'); + $I->see('Email cannot be blank.', '.help-block'); + $I->see('Password cannot be blank.', '.help-block'); - $I->amGoingTo('submit signup form with not correct email'); - $signupPage->submit([ - 'username' => 'tester', - 'email' => 'tester.email', - 'password' => 'tester_password', - ]); + $I->amGoingTo('submit signup form with not correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email', + 'password' => 'tester_password', + ]); - $I->expectTo('see that email address is wrong'); - $I->dontSee('Username cannot be blank.', '.help-block'); - $I->dontSee('Password cannot be blank.', '.help-block'); - $I->see('Email is not a valid email address.', '.help-block'); + $I->expectTo('see that email address is wrong'); + $I->dontSee('Username cannot be blank.', '.help-block'); + $I->dontSee('Password cannot be blank.', '.help-block'); + $I->see('Email is not a valid email address.', '.help-block'); - $I->amGoingTo('submit signup form with correct email'); - $signupPage->submit([ - 'username' => 'tester', - 'email' => 'tester.email@example.com', - 'password' => 'tester_password', - ]); + $I->amGoingTo('submit signup form with correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email@example.com', + 'password' => 'tester_password', + ]); - $I->expectTo('see that user logged in'); - $I->seeLink('Logout (tester)'); - } + $I->expectTo('see that user logged in'); + $I->seeLink('Logout (tester)'); + } } diff --git a/apps/advanced/frontend/tests/acceptance/_config.php b/apps/advanced/frontend/tests/acceptance/_config.php index 4a70bb96815..ce42793f3fd 100644 --- a/apps/advanced/frontend/tests/acceptance/_config.php +++ b/apps/advanced/frontend/tests/acceptance/_config.php @@ -1,16 +1,16 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', - ], - ], - ] + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', + ], + ], + ] ); diff --git a/apps/advanced/frontend/tests/acceptance/_console.php b/apps/advanced/frontend/tests/acceptance/_console.php index 1e1ec5611c5..bae7e44b03f 100644 --- a/apps/advanced/frontend/tests/acceptance/_console.php +++ b/apps/advanced/frontend/tests/acceptance/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_acceptance', + ], + ], + ] ); diff --git a/apps/advanced/frontend/tests/functional/ContactCept.php b/apps/advanced/frontend/tests/functional/ContactCept.php index b29b72f2185..c08e8e0c4ba 100644 --- a/apps/advanced/frontend/tests/functional/ContactCept.php +++ b/apps/advanced/frontend/tests/functional/ContactCept.php @@ -21,11 +21,11 @@ $I->amGoingTo('submit contact form with not correct email'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester.email', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester.email', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->expectTo('see that email adress is wrong'); $I->dontSee('Name cannot be blank', '.help-block'); @@ -36,10 +36,10 @@ $I->amGoingTo('submit contact form with correct data'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester@example.com', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester@example.com', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/advanced/frontend/tests/functional/SignupCest.php b/apps/advanced/frontend/tests/functional/SignupCest.php index 86efff4d3fc..5287aeec6ad 100644 --- a/apps/advanced/frontend/tests/functional/SignupCest.php +++ b/apps/advanced/frontend/tests/functional/SignupCest.php @@ -8,83 +8,83 @@ class SignupCest { - /** - * This method is called before each cest class test method - * @param \Codeception\Event\Test $event - */ - public function _before($event) - { - } - - /** - * This method is called after each cest class test method, even if test failed. - * @param \Codeception\Event\Test $event - */ - public function _after($event) - { - User::deleteAll([ - 'email' => 'tester.email@example.com', - 'username' => 'tester', - ]); - } - - /** - * This method is called when test fails. - * @param \Codeception\Event\Fail $event - */ - public function _fail($event) - { - - } - - /** - * - * @param \TestGuy $I - * @param \Codeception\Scenario $scenario - */ - public function testUserSignup($I, $scenario) - { - $I->wantTo('ensure that signup works'); - - $signupPage = SignupPage::openBy($I); - $I->see('Signup', 'h1'); - $I->see('Please fill out the following fields to signup:'); - - $I->amGoingTo('submit signup form with no data'); - - $signupPage->submit([]); - - $I->expectTo('see validation errors'); - $I->see('Username cannot be blank.', '.help-block'); - $I->see('Email cannot be blank.', '.help-block'); - $I->see('Password cannot be blank.', '.help-block'); - - $I->amGoingTo('submit signup form with not correct email'); - $signupPage->submit([ - 'username' => 'tester', - 'email' => 'tester.email', - 'password' => 'tester_password', - ]); - - $I->expectTo('see that email address is wrong'); - $I->dontSee('Username cannot be blank.', '.help-block'); - $I->dontSee('Password cannot be blank.', '.help-block'); - $I->see('Email is not a valid email address.', '.help-block'); - - $I->amGoingTo('submit signup form with correct email'); - $signupPage->submit([ - 'username' => 'tester', - 'email' => 'tester.email@example.com', - 'password' => 'tester_password', - ]); - - $I->expectTo('see that user is created'); - $I->seeRecord('common\models\User', [ - 'username' => 'tester', - 'email' => 'tester.email@example.com', - ]); - - $I->expectTo('see that user logged in'); - $I->seeLink('Logout (tester)'); - } + /** + * This method is called before each cest class test method + * @param \Codeception\Event\Test $event + */ + public function _before($event) + { + } + + /** + * This method is called after each cest class test method, even if test failed. + * @param \Codeception\Event\Test $event + */ + public function _after($event) + { + User::deleteAll([ + 'email' => 'tester.email@example.com', + 'username' => 'tester', + ]); + } + + /** + * This method is called when test fails. + * @param \Codeception\Event\Fail $event + */ + public function _fail($event) + { + + } + + /** + * + * @param \TestGuy $I + * @param \Codeception\Scenario $scenario + */ + public function testUserSignup($I, $scenario) + { + $I->wantTo('ensure that signup works'); + + $signupPage = SignupPage::openBy($I); + $I->see('Signup', 'h1'); + $I->see('Please fill out the following fields to signup:'); + + $I->amGoingTo('submit signup form with no data'); + + $signupPage->submit([]); + + $I->expectTo('see validation errors'); + $I->see('Username cannot be blank.', '.help-block'); + $I->see('Email cannot be blank.', '.help-block'); + $I->see('Password cannot be blank.', '.help-block'); + + $I->amGoingTo('submit signup form with not correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email', + 'password' => 'tester_password', + ]); + + $I->expectTo('see that email address is wrong'); + $I->dontSee('Username cannot be blank.', '.help-block'); + $I->dontSee('Password cannot be blank.', '.help-block'); + $I->see('Email is not a valid email address.', '.help-block'); + + $I->amGoingTo('submit signup form with correct email'); + $signupPage->submit([ + 'username' => 'tester', + 'email' => 'tester.email@example.com', + 'password' => 'tester_password', + ]); + + $I->expectTo('see that user is created'); + $I->seeRecord('common\models\User', [ + 'username' => 'tester', + 'email' => 'tester.email@example.com', + ]); + + $I->expectTo('see that user logged in'); + $I->seeLink('Logout (tester)'); + } } diff --git a/apps/advanced/frontend/tests/functional/_config.php b/apps/advanced/frontend/tests/functional/_config.php index c1bc080f567..ba776b6169f 100644 --- a/apps/advanced/frontend/tests/functional/_config.php +++ b/apps/advanced/frontend/tests/functional/_config.php @@ -5,16 +5,16 @@ $_SERVER['SCRIPT_NAME'] = TEST_ENTRY_URL; return yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../config/main.php'), - require(__DIR__ . '/../../config/main-local.php'), - require(__DIR__ . '/../../../common/config/main.php'), - require(__DIR__ . '/../../../common/config/main-local.php'), - require(__DIR__ . '/../_config.php'), - [ - 'components' => [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', - ], - ], - ] + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', + ], + ], + ] ); diff --git a/apps/advanced/frontend/tests/functional/_console.php b/apps/advanced/frontend/tests/functional/_console.php index 39434f7a9d1..d76662c4e51 100644 --- a/apps/advanced/frontend/tests/functional/_console.php +++ b/apps/advanced/frontend/tests/functional/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_functional', + ], + ], + ] ); diff --git a/apps/advanced/frontend/tests/unit/DbTestCase.php b/apps/advanced/frontend/tests/unit/DbTestCase.php index 998d5b83a7b..f175a3b9fda 100644 --- a/apps/advanced/frontend/tests/unit/DbTestCase.php +++ b/apps/advanced/frontend/tests/unit/DbTestCase.php @@ -4,5 +4,5 @@ class DbTestCase extends \yii\codeception\DbTestCase { - public $appConfig = '@frontend/tests/unit/_config.php'; + public $appConfig = '@frontend/tests/unit/_config.php'; } diff --git a/apps/advanced/frontend/tests/unit/TestCase.php b/apps/advanced/frontend/tests/unit/TestCase.php index 721408cecb2..d8a32b6e69f 100644 --- a/apps/advanced/frontend/tests/unit/TestCase.php +++ b/apps/advanced/frontend/tests/unit/TestCase.php @@ -4,5 +4,5 @@ class TestCase extends \yii\codeception\TestCase { - public $appConfig = '@frontend/tests/unit/_config.php'; + public $appConfig = '@frontend/tests/unit/_config.php'; } diff --git a/apps/advanced/frontend/tests/unit/_config.php b/apps/advanced/frontend/tests/unit/_config.php index 22e5c6245c8..97ed57b0f23 100644 --- a/apps/advanced/frontend/tests/unit/_config.php +++ b/apps/advanced/frontend/tests/unit/_config.php @@ -1,16 +1,16 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../config/main.php'), + require(__DIR__ . '/../../config/main-local.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/frontend/tests/unit/_console.php b/apps/advanced/frontend/tests/unit/_console.php index a0d8d02284f..1060e27de4e 100644 --- a/apps/advanced/frontend/tests/unit/_console.php +++ b/apps/advanced/frontend/tests/unit/_console.php @@ -1,15 +1,15 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', - ], - ], - ] + require(__DIR__ . '/../../../common/config/main.php'), + require(__DIR__ . '/../../../common/config/main-local.php'), + require(__DIR__ . '/../../../console/config/main.php'), + require(__DIR__ . '/../../../console/config/main-local.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_advanced_unit', + ], + ], + ] ); diff --git a/apps/advanced/frontend/tests/unit/fixtures/data/tbl_user.php b/apps/advanced/frontend/tests/unit/fixtures/data/tbl_user.php index 69552c25854..67ba32f1c4e 100644 --- a/apps/advanced/frontend/tests/unit/fixtures/data/tbl_user.php +++ b/apps/advanced/frontend/tests/unit/fixtures/data/tbl_user.php @@ -1,23 +1,23 @@ 'okirlin', - 'auth_key' => 'iwTNae9t34OmnK6l4vT4IeaTk-YWI2Rv', - 'password_hash' => '$2y$13$CXT0Rkle1EMJ/c1l5bylL.EylfmQ39O5JlHJVFpNn618OUS1HwaIi', - 'password_reset_token' => 't5GU9NwpuGYSfb7FEZMAxqtuz2PkEvv_1391885313', - 'created_at' => '1391885313', - 'updated_at' => '1391885313', - 'email' => 'brady.renner@rutherford.com', - ], - [ - 'username' => 'troy.becker', - 'auth_key' => 'EdKfXrx88weFMV0vIxuTMWKgfK2tS3Lp', - 'password_hash' => '$2y$13$g5nv41Px7VBqhS3hVsVN2.MKfgT3jFdkXEsMC4rQJLfaMa7VaJqL2', - 'password_reset_token' => '4BSNyiZNAuxjs5Mty990c47sVrgllIi_1391885313', - 'created_at' => '1391885313', - 'updated_at' => '1391885313', - 'email' => 'nicolas.dianna@hotmail.com', - 'status' => '0', - ], + [ + 'username' => 'okirlin', + 'auth_key' => 'iwTNae9t34OmnK6l4vT4IeaTk-YWI2Rv', + 'password_hash' => '$2y$13$CXT0Rkle1EMJ/c1l5bylL.EylfmQ39O5JlHJVFpNn618OUS1HwaIi', + 'password_reset_token' => 't5GU9NwpuGYSfb7FEZMAxqtuz2PkEvv_1391885313', + 'created_at' => '1391885313', + 'updated_at' => '1391885313', + 'email' => 'brady.renner@rutherford.com', + ], + [ + 'username' => 'troy.becker', + 'auth_key' => 'EdKfXrx88weFMV0vIxuTMWKgfK2tS3Lp', + 'password_hash' => '$2y$13$g5nv41Px7VBqhS3hVsVN2.MKfgT3jFdkXEsMC4rQJLfaMa7VaJqL2', + 'password_reset_token' => '4BSNyiZNAuxjs5Mty990c47sVrgllIi_1391885313', + 'created_at' => '1391885313', + 'updated_at' => '1391885313', + 'email' => 'nicolas.dianna@hotmail.com', + 'status' => '0', + ], ]; diff --git a/apps/advanced/frontend/tests/unit/models/ContactFormTest.php b/apps/advanced/frontend/tests/unit/models/ContactFormTest.php index 28f17476c0d..6086cb9c067 100644 --- a/apps/advanced/frontend/tests/unit/models/ContactFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/ContactFormTest.php @@ -9,51 +9,51 @@ class ContactFormTest extends TestCase { - use \Codeception\Specify; - - protected function setUp() - { - parent::setUp(); - Yii::$app->mail->fileTransportCallback = function ($mailer, $message) { - return 'testing_message.eml'; - }; - } - - protected function tearDown() - { - unlink($this->getMessageFile()); - parent::tearDown(); - } - - public function testContact() - { - $model = new ContactForm(); - - $model->attributes = [ - 'name' => 'Tester', - 'email' => 'tester@example.com', - 'subject' => 'very important letter subject', - 'body' => 'body of current message', - ]; - - $model->sendEmail('admin@example.com'); - - $this->specify('email should be send', function () { - expect('email file should exist', file_exists($this->getMessageFile()))->true(); - }); - - $this->specify('message should contain correct data', function () use ($model) { - $emailMessage = file_get_contents($this->getMessageFile()); - - expect('email should contain user name', $emailMessage)->contains($model->name); - expect('email should contain sender email', $emailMessage)->contains($model->email); - expect('email should contain subject', $emailMessage)->contains($model->subject); - expect('email should contain body', $emailMessage)->contains($model->body); - }); - } - - private function getMessageFile() - { - return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; - } + use \Codeception\Specify; + + protected function setUp() + { + parent::setUp(); + Yii::$app->mail->fileTransportCallback = function ($mailer, $message) { + return 'testing_message.eml'; + }; + } + + protected function tearDown() + { + unlink($this->getMessageFile()); + parent::tearDown(); + } + + public function testContact() + { + $model = new ContactForm(); + + $model->attributes = [ + 'name' => 'Tester', + 'email' => 'tester@example.com', + 'subject' => 'very important letter subject', + 'body' => 'body of current message', + ]; + + $model->sendEmail('admin@example.com'); + + $this->specify('email should be send', function () { + expect('email file should exist', file_exists($this->getMessageFile()))->true(); + }); + + $this->specify('message should contain correct data', function () use ($model) { + $emailMessage = file_get_contents($this->getMessageFile()); + + expect('email should contain user name', $emailMessage)->contains($model->name); + expect('email should contain sender email', $emailMessage)->contains($model->email); + expect('email should contain subject', $emailMessage)->contains($model->subject); + expect('email should contain body', $emailMessage)->contains($model->body); + }); + } + + private function getMessageFile() + { + return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; + } } diff --git a/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php b/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php index 7591ff32f2b..9eb6e6d8b04 100644 --- a/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/PasswordResetRequestFormTest.php @@ -10,69 +10,69 @@ class PasswordResetRequestFormTest extends DbTestCase { - use \Codeception\Specify; + use \Codeception\Specify; - protected function setUp() - { - parent::setUp(); - Yii::$app->mail->fileTransportCallback = function ($mailer, $message) { - return 'testing_message.eml'; - }; - } + protected function setUp() + { + parent::setUp(); + Yii::$app->mail->fileTransportCallback = function ($mailer, $message) { + return 'testing_message.eml'; + }; + } - protected function tearDown() - { - @unlink($this->getMessageFile()); - parent::tearDown(); - } + protected function tearDown() + { + @unlink($this->getMessageFile()); + parent::tearDown(); + } - public function testSendEmailWrongUser() - { - $this->specify('no user with such email, message should not be send', function () { - $model = new PasswordResetRequestForm(); - $model->email = 'not-existing-email@example.com'; - - expect('email not send', $model->sendEmail())->false(); - }); + public function testSendEmailWrongUser() + { + $this->specify('no user with such email, message should not be send', function () { + $model = new PasswordResetRequestForm(); + $model->email = 'not-existing-email@example.com'; - $this->specify('user is not active, message should not be send', function () { - $model = new PasswordResetRequestForm(); - $model->email = $this->user[1]['email']; - - expect('email not send', $model->sendEmail())->false(); - }); - } - - public function testSendEmailCorrectUser() - { - $model = new PasswordResetRequestForm(); - $model->email = $this->user[0]['email']; - $user = User::find(['password_reset_token' => $this->user[0]['password_reset_token']]); + expect('email not send', $model->sendEmail())->false(); + }); - expect('email sent', $model->sendEmail())->true(); - expect('user has valid token', $user->password_reset_token)->notNull(); - - $this->specify('message has correct format', function () use ($model) { - expect('message file exists', file_exists($this->getMessageFile()))->true(); + $this->specify('user is not active, message should not be send', function () { + $model = new PasswordResetRequestForm(); + $model->email = $this->user[1]['email']; - $message = file_get_contents($this->getMessageFile()); - expect('message "from" is correct', $message)->contains(Yii::$app->params['supportEmail']); - expect('message "to" is correct', $message)->contains($model->email); - }); - } + expect('email not send', $model->sendEmail())->false(); + }); + } - public function fixtures() - { - return [ - 'user' => [ - 'class' => UserFixture::className(), - 'dataFile' => '@frontend/tests/unit/fixtures/data/tbl_user.php' - ], - ]; - } + public function testSendEmailCorrectUser() + { + $model = new PasswordResetRequestForm(); + $model->email = $this->user[0]['email']; + $user = User::find(['password_reset_token' => $this->user[0]['password_reset_token']]); - private function getMessageFile() - { - return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; - } + expect('email sent', $model->sendEmail())->true(); + expect('user has valid token', $user->password_reset_token)->notNull(); + + $this->specify('message has correct format', function () use ($model) { + expect('message file exists', file_exists($this->getMessageFile()))->true(); + + $message = file_get_contents($this->getMessageFile()); + expect('message "from" is correct', $message)->contains(Yii::$app->params['supportEmail']); + expect('message "to" is correct', $message)->contains($model->email); + }); + } + + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@frontend/tests/unit/fixtures/data/tbl_user.php' + ], + ]; + } + + private function getMessageFile() + { + return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; + } } diff --git a/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php b/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php index b00c2acf359..f892a2ce90c 100644 --- a/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/ResetPasswordFormTest.php @@ -9,28 +9,28 @@ class ResetPasswordFormTest extends DbTestCase { - use \Codeception\Specify; + use \Codeception\Specify; - public function testResetPassword() - { - $this->specify('wrong reset token', function () { - $this->setExpectedException('\Exception', 'Wrong password reset token.'); - new ResetPasswordForm('notexistingtoken_1391882543'); - }); + public function testResetPassword() + { + $this->specify('wrong reset token', function () { + $this->setExpectedException('\Exception', 'Wrong password reset token.'); + new ResetPasswordForm('notexistingtoken_1391882543'); + }); - $this->specify('not correct token', function () { - $this->setExpectedException('yii\base\InvalidParamException', 'Password reset token cannot be blank.'); - new ResetPasswordForm(''); - }); - } + $this->specify('not correct token', function () { + $this->setExpectedException('yii\base\InvalidParamException', 'Password reset token cannot be blank.'); + new ResetPasswordForm(''); + }); + } - public function fixtures() - { - return [ - 'user' => [ - 'class' => UserFixture::className(), - 'dataFile' => '@frontend/tests/unit/fixtures/data/tbl_user.php' - ], - ]; - } + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => '@frontend/tests/unit/fixtures/data/tbl_user.php' + ], + ]; + } } diff --git a/apps/advanced/frontend/tests/unit/models/SignupFormTest.php b/apps/advanced/frontend/tests/unit/models/SignupFormTest.php index 359a02c1865..c38f5dcb9c6 100644 --- a/apps/advanced/frontend/tests/unit/models/SignupFormTest.php +++ b/apps/advanced/frontend/tests/unit/models/SignupFormTest.php @@ -8,39 +8,39 @@ class SignupFormTest extends DbTestCase { - use \Codeception\Specify; - - public function testCorrectSignup() - { - $model = $this->getMock('frontend\models\SignupForm', ['validate']); - $model->expects($this->once())->method('validate')->will($this->returnValue(true)); - - $model->username = 'some_username'; - $model->email = 'some_email@example.com'; - $model->password = 'some_password'; - - $user = $model->signup(); - $this->assertInstanceOf('common\models\User', $user); - expect('username should be correct', $user->username)->equals('some_username'); - expect('email should be correct', $user->email)->equals('some_email@example.com'); - expect('password should be correct', $user->validatePassword('some_password'))->true(); - } - - public function testNotCorrectSignup() - { - $model = $this->getMock('frontend\models\SignupForm', ['validate']); - $model->expects($this->once())->method('validate')->will($this->returnValue(false)); - - expect('user should not be created', $model->signup())->null(); - } - - public function fixtures() - { - return [ - 'user' => [ - 'class' => UserFixture::className(), - 'dataFile' => false, //do not load test data, only table cleanup - ], - ]; - } + use \Codeception\Specify; + + public function testCorrectSignup() + { + $model = $this->getMock('frontend\models\SignupForm', ['validate']); + $model->expects($this->once())->method('validate')->will($this->returnValue(true)); + + $model->username = 'some_username'; + $model->email = 'some_email@example.com'; + $model->password = 'some_password'; + + $user = $model->signup(); + $this->assertInstanceOf('common\models\User', $user); + expect('username should be correct', $user->username)->equals('some_username'); + expect('email should be correct', $user->email)->equals('some_email@example.com'); + expect('password should be correct', $user->validatePassword('some_password'))->true(); + } + + public function testNotCorrectSignup() + { + $model = $this->getMock('frontend\models\SignupForm', ['validate']); + $model->expects($this->once())->method('validate')->will($this->returnValue(false)); + + expect('user should not be created', $model->signup())->null(); + } + + public function fixtures() + { + return [ + 'user' => [ + 'class' => UserFixture::className(), + 'dataFile' => false, //do not load test data, only table cleanup + ], + ]; + } } diff --git a/apps/advanced/frontend/views/layouts/main.php b/apps/advanced/frontend/views/layouts/main.php index 6e3ae8c4c2e..df9fa6c9da4 100644 --- a/apps/advanced/frontend/views/layouts/main.php +++ b/apps/advanced/frontend/views/layouts/main.php @@ -16,61 +16,61 @@ - - - <?= Html::encode($this->title) ?> - head() ?> + + + <?= Html::encode($this->title) ?> + head() ?> - beginBody() ?> -
- 'My Company', - 'brandUrl' => Yii::$app->homeUrl, - 'options' => [ - 'class' => 'navbar-inverse navbar-fixed-top', - ], - ]); - $menuItems = [ - ['label' => 'Home', 'url' => ['/site/index']], - ['label' => 'About', 'url' => ['/site/about']], - ['label' => 'Contact', 'url' => ['/site/contact']], - ]; - if (Yii::$app->user->isGuest) { - $menuItems[] = ['label' => 'Signup', 'url' => ['/site/signup']]; - $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; - } else { - $menuItems[] = [ - 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', - 'url' => ['/site/logout'], - 'linkOptions' => ['data-method' => 'post'] - ]; - } - echo Nav::widget([ - 'options' => ['class' => 'navbar-nav navbar-right'], - 'items' => $menuItems, - ]); - NavBar::end(); - ?> + beginBody() ?> +
+ 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => [ + 'class' => 'navbar-inverse navbar-fixed-top', + ], + ]); + $menuItems = [ + ['label' => 'Home', 'url' => ['/site/index']], + ['label' => 'About', 'url' => ['/site/about']], + ['label' => 'Contact', 'url' => ['/site/contact']], + ]; + if (Yii::$app->user->isGuest) { + $menuItems[] = ['label' => 'Signup', 'url' => ['/site/signup']]; + $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; + } else { + $menuItems[] = [ + 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', + 'url' => ['/site/logout'], + 'linkOptions' => ['data-method' => 'post'] + ]; + } + echo Nav::widget([ + 'options' => ['class' => 'navbar-nav navbar-right'], + 'items' => $menuItems, + ]); + NavBar::end(); + ?> -
- isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], - ]) ?> - - -
-
- -
-
-

© My Company

-

-
-
+
+ isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], + ]) ?> + + +
+
- endBody() ?> + + + endBody() ?> endPage() ?> diff --git a/apps/advanced/frontend/views/site/about.php b/apps/advanced/frontend/views/site/about.php index ac4ab24a2b8..db855ca068a 100644 --- a/apps/advanced/frontend/views/site/about.php +++ b/apps/advanced/frontend/views/site/about.php @@ -8,9 +8,9 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

This is the About page. You may modify the following file to customize its content:

+

This is the About page. You may modify the following file to customize its content:

- +
diff --git a/apps/advanced/frontend/views/site/contact.php b/apps/advanced/frontend/views/site/contact.php index 8248f3f1b16..e83939d0dfe 100644 --- a/apps/advanced/frontend/views/site/contact.php +++ b/apps/advanced/frontend/views/site/contact.php @@ -12,27 +12,27 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

- If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. -

+

+ If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. +

-
-
- 'contact-form']); ?> - field($model, 'name') ?> - field($model, 'email') ?> - field($model, 'subject') ?> - field($model, 'body')->textArea(['rows' => 6]) ?> - field($model, 'verifyCode')->widget(Captcha::className(), [ - 'template' => '
{image}
{input}
', - ]) ?> -
- 'btn btn-primary', 'name' => 'contact-button']) ?> -
- -
-
+
+
+ 'contact-form']); ?> + field($model, 'name') ?> + field($model, 'email') ?> + field($model, 'subject') ?> + field($model, 'body')->textArea(['rows' => 6]) ?> + field($model, 'verifyCode')->widget(Captcha::className(), [ + 'template' => '
{image}
{input}
', + ]) ?> +
+ 'btn btn-primary', 'name' => 'contact-button']) ?> +
+ +
+
diff --git a/apps/advanced/frontend/views/site/error.php b/apps/advanced/frontend/views/site/error.php index 1b7ce0422e6..c172fd621a3 100644 --- a/apps/advanced/frontend/views/site/error.php +++ b/apps/advanced/frontend/views/site/error.php @@ -13,17 +13,17 @@ ?>
-

title) ?>

+

title) ?>

-
- -
+
+ +
-

- The above error occurred while the Web server was processing your request. -

-

- Please contact us if you think this is a server error. Thank you. -

+

+ The above error occurred while the Web server was processing your request. +

+

+ Please contact us if you think this is a server error. Thank you. +

diff --git a/apps/advanced/frontend/views/site/index.php b/apps/advanced/frontend/views/site/index.php index bcb22781a60..6b6394e5626 100644 --- a/apps/advanced/frontend/views/site/index.php +++ b/apps/advanced/frontend/views/site/index.php @@ -6,48 +6,48 @@ ?>
-
-

Congratulations!

+
+

Congratulations!

-

You have successfully created your Yii-powered application.

+

You have successfully created your Yii-powered application.

-

Get started with Yii

-
+

Get started with Yii

+
-
+
-
-
-

Heading

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Documentation »

-
-
-

Heading

+

Yii Documentation »

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Forum »

-
-
-

Heading

+

Yii Forum »

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Extensions »

-
-
+

Yii Extensions »

+
+
-
+
diff --git a/apps/advanced/frontend/views/site/login.php b/apps/advanced/frontend/views/site/login.php index ffe70c15a36..046c5a7afdf 100644 --- a/apps/advanced/frontend/views/site/login.php +++ b/apps/advanced/frontend/views/site/login.php @@ -11,23 +11,23 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

Please fill out the following fields to login:

+

Please fill out the following fields to login:

-
-
- 'login-form']); ?> - field($model, 'username') ?> - field($model, 'password')->passwordInput() ?> - field($model, 'rememberMe')->checkbox() ?> -
- If you forgot your password you can . -
-
- 'btn btn-primary', 'name' => 'login-button']) ?> -
- -
-
+
+
+ 'login-form']); ?> + field($model, 'username') ?> + field($model, 'password')->passwordInput() ?> + field($model, 'rememberMe')->checkbox() ?> +
+ If you forgot your password you can . +
+
+ 'btn btn-primary', 'name' => 'login-button']) ?> +
+ +
+
diff --git a/apps/advanced/frontend/views/site/requestPasswordResetToken.php b/apps/advanced/frontend/views/site/requestPasswordResetToken.php index fc22aa19418..97ef91624bf 100644 --- a/apps/advanced/frontend/views/site/requestPasswordResetToken.php +++ b/apps/advanced/frontend/views/site/requestPasswordResetToken.php @@ -11,18 +11,18 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

Please fill out your email. A link to reset password will be sent there.

+

Please fill out your email. A link to reset password will be sent there.

-
-
- 'request-password-reset-form']); ?> - field($model, 'email') ?> -
- 'btn btn-primary']) ?> -
- -
-
+
+
+ 'request-password-reset-form']); ?> + field($model, 'email') ?> +
+ 'btn btn-primary']) ?> +
+ +
+
diff --git a/apps/advanced/frontend/views/site/resetPassword.php b/apps/advanced/frontend/views/site/resetPassword.php index 170b703b5f1..04c8108c410 100644 --- a/apps/advanced/frontend/views/site/resetPassword.php +++ b/apps/advanced/frontend/views/site/resetPassword.php @@ -11,18 +11,18 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

Please choose your new password:

+

Please choose your new password:

-
-
- 'reset-password-form']); ?> - field($model, 'password')->passwordInput() ?> -
- 'btn btn-primary']) ?> -
- -
-
+
+
+ 'reset-password-form']); ?> + field($model, 'password')->passwordInput() ?> +
+ 'btn btn-primary']) ?> +
+ +
+
diff --git a/apps/advanced/frontend/views/site/signup.php b/apps/advanced/frontend/views/site/signup.php index 795d03d5849..a5cd07a2452 100644 --- a/apps/advanced/frontend/views/site/signup.php +++ b/apps/advanced/frontend/views/site/signup.php @@ -11,20 +11,20 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

Please fill out the following fields to signup:

+

Please fill out the following fields to signup:

-
-
- 'form-signup']); ?> - field($model, 'username') ?> - field($model, 'email') ?> - field($model, 'password')->passwordInput() ?> -
- 'btn btn-primary', 'name' => 'signup-button']) ?> -
- -
-
+
+
+ 'form-signup']); ?> + field($model, 'username') ?> + field($model, 'email') ?> + field($model, 'password')->passwordInput() ?> +
+ 'btn btn-primary', 'name' => 'signup-button']) ?> +
+ +
+
diff --git a/apps/advanced/frontend/widgets/Alert.php b/apps/advanced/frontend/widgets/Alert.php index a833855da91..b4c7fb19943 100644 --- a/apps/advanced/frontend/widgets/Alert.php +++ b/apps/advanced/frontend/widgets/Alert.php @@ -20,49 +20,49 @@ */ class Alert extends \yii\bootstrap\Widget { - /** - * @var array the alert types configuration for the flash messages. - * This array is setup as $key => $value, where: - * - $key is the name of the session flash variable - * - $value is the bootstrap alert type (i.e. danger, success, info, warning) - */ - public $alertTypes = [ - 'error' => 'alert-danger', - 'danger' => 'alert-danger', - 'success' => 'alert-success', - 'info' => 'alert-info', - 'warning' => 'alert-warning' - ]; + /** + * @var array the alert types configuration for the flash messages. + * This array is setup as $key => $value, where: + * - $key is the name of the session flash variable + * - $value is the bootstrap alert type (i.e. danger, success, info, warning) + */ + public $alertTypes = [ + 'error' => 'alert-danger', + 'danger' => 'alert-danger', + 'success' => 'alert-success', + 'info' => 'alert-info', + 'warning' => 'alert-warning' + ]; - /** - * @var array the options for rendering the close button tag. - */ - public $closeButton = []; + /** + * @var array the options for rendering the close button tag. + */ + public $closeButton = []; - public function init() - { - parent::init(); + public function init() + { + parent::init(); - $session = \Yii::$app->getSession(); - $flashes = $session->getAllFlashes(); - $appendCss = isset($this->options['class']) ? ' ' . $this->options['class'] : ''; + $session = \Yii::$app->getSession(); + $flashes = $session->getAllFlashes(); + $appendCss = isset($this->options['class']) ? ' ' . $this->options['class'] : ''; - foreach ($flashes as $type => $message) { - if (isset($this->alertTypes[$type])) { - /* initialize css class for each alert box */ - $this->options['class'] = $this->alertTypes[$type] . $appendCss; + foreach ($flashes as $type => $message) { + if (isset($this->alertTypes[$type])) { + /* initialize css class for each alert box */ + $this->options['class'] = $this->alertTypes[$type] . $appendCss; - /* assign unique id to each alert box */ - $this->options['id'] = $this->getId() . '-' . $type; + /* assign unique id to each alert box */ + $this->options['id'] = $this->getId() . '-' . $type; - echo \yii\bootstrap\Alert::widget([ - 'body' => $message, - 'closeButton' => $this->closeButton, - 'options' => $this->options, - ]); + echo \yii\bootstrap\Alert::widget([ + 'body' => $message, + 'closeButton' => $this->closeButton, + 'options' => $this->options, + ]); - $session->removeFlash($type); - } - } - } + $session->removeFlash($type); + } + } + } } diff --git a/apps/advanced/requirements.php b/apps/advanced/requirements.php index d470f99d7be..b96e4d69dcb 100644 --- a/apps/advanced/requirements.php +++ b/apps/advanced/requirements.php @@ -14,10 +14,10 @@ $frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2'; if (!is_dir($frameworkPath)) { - echo '

Error

'; - echo '

The path to yii framework seems to be incorrect.

'; - echo '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . '.

'; - echo '

Please refer to the README on how to install Yii.

'; + echo '

Error

'; + echo '

The path to yii framework seems to be incorrect.

'; + echo '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . '.

'; + echo '

Please refer to the README on how to install Yii.

'; } require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); @@ -27,84 +27,84 @@ * Adjust requirements according to your application specifics. */ $requirements = array( - // Database : - array( - 'name' => 'PDO extension', - 'mandatory' => true, - 'condition' => extension_loaded('pdo'), - 'by' => 'All DB-related classes', - ), - array( - 'name' => 'PDO SQLite extension', - 'mandatory' => false, - 'condition' => extension_loaded('pdo_sqlite'), - 'by' => 'All DB-related classes', - 'memo' => 'Required for SQLite database.', - ), - array( - 'name' => 'PDO MySQL extension', - 'mandatory' => false, - 'condition' => extension_loaded('pdo_mysql'), - 'by' => 'All DB-related classes', - 'memo' => 'Required for MySQL database.', - ), - array( - 'name' => 'PDO PostgreSQL extension', - 'mandatory' => false, - 'condition' => extension_loaded('pdo_pgsql'), - 'by' => 'All DB-related classes', - 'memo' => 'Required for PostgreSQL database.', - ), - // Cache : - array( - 'name' => 'Memcache extension', - 'mandatory' => false, - 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), - 'by' => 'CMemCache', - 'memo' => extension_loaded('memcached') ? 'To use memcached set CMemCache::useMemcached to true.' : '' - ), - array( - 'name' => 'APC extension', - 'mandatory' => false, - 'condition' => extension_loaded('apc'), - 'by' => 'CApcCache', - ), - // Additional PHP extensions : - array( - 'name' => 'Mcrypt extension', - 'mandatory' => false, - 'condition' => extension_loaded('mcrypt'), - 'by' => 'CSecurityManager', - 'memo' => 'Required by encrypt and decrypt methods.' - ), - // PHP ini : - 'phpSafeMode' => array( - 'name' => 'PHP safe mode', - 'mandatory' => false, - 'condition' => $requirementsChecker->checkPhpIniOff("safe_mode"), - 'by' => 'File uploading and console command execution', - 'memo' => '"safe_mode" should be disabled at php.ini', - ), - 'phpExposePhp' => array( - 'name' => 'Expose PHP', - 'mandatory' => false, - 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), - 'by' => 'Security reasons', - 'memo' => '"expose_php" should be disabled at php.ini', - ), - 'phpAllowUrlInclude' => array( - 'name' => 'PHP allow url include', - 'mandatory' => false, - 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), - 'by' => 'Security reasons', - 'memo' => '"allow_url_include" should be disabled at php.ini', - ), - 'phpSmtp' => array( - 'name' => 'PHP mail SMTP', - 'mandatory' => false, - 'condition' => strlen(ini_get('SMTP'))>0, - 'by' => 'Email sending', - 'memo' => 'PHP mail SMTP server required', - ), + // Database : + array( + 'name' => 'PDO extension', + 'mandatory' => true, + 'condition' => extension_loaded('pdo'), + 'by' => 'All DB-related classes', + ), + array( + 'name' => 'PDO SQLite extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_sqlite'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for SQLite database.', + ), + array( + 'name' => 'PDO MySQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_mysql'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for MySQL database.', + ), + array( + 'name' => 'PDO PostgreSQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_pgsql'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for PostgreSQL database.', + ), + // Cache : + array( + 'name' => 'Memcache extension', + 'mandatory' => false, + 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), + 'by' => 'CMemCache', + 'memo' => extension_loaded('memcached') ? 'To use memcached set CMemCache::useMemcached to true.' : '' + ), + array( + 'name' => 'APC extension', + 'mandatory' => false, + 'condition' => extension_loaded('apc'), + 'by' => 'CApcCache', + ), + // Additional PHP extensions : + array( + 'name' => 'Mcrypt extension', + 'mandatory' => false, + 'condition' => extension_loaded('mcrypt'), + 'by' => 'CSecurityManager', + 'memo' => 'Required by encrypt and decrypt methods.' + ), + // PHP ini : + 'phpSafeMode' => array( + 'name' => 'PHP safe mode', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("safe_mode"), + 'by' => 'File uploading and console command execution', + 'memo' => '"safe_mode" should be disabled at php.ini', + ), + 'phpExposePhp' => array( + 'name' => 'Expose PHP', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), + 'by' => 'Security reasons', + 'memo' => '"expose_php" should be disabled at php.ini', + ), + 'phpAllowUrlInclude' => array( + 'name' => 'PHP allow url include', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), + 'by' => 'Security reasons', + 'memo' => '"allow_url_include" should be disabled at php.ini', + ), + 'phpSmtp' => array( + 'name' => 'PHP mail SMTP', + 'mandatory' => false, + 'condition' => strlen(ini_get('SMTP'))>0, + 'by' => 'Email sending', + 'memo' => 'PHP mail SMTP server required', + ), ); $requirementsChecker->checkYii()->check($requirements)->render(); diff --git a/apps/basic/assets/AppAsset.php b/apps/basic/assets/AppAsset.php index c964d36a68d..0e495a8f8ac 100644 --- a/apps/basic/assets/AppAsset.php +++ b/apps/basic/assets/AppAsset.php @@ -15,15 +15,15 @@ */ class AppAsset extends AssetBundle { - public $basePath = '@webroot'; - public $baseUrl = '@web'; - public $css = [ - 'css/site.css', - ]; - public $js = [ - ]; - public $depends = [ - 'yii\web\YiiAsset', - 'yii\bootstrap\BootstrapAsset', - ]; + public $basePath = '@webroot'; + public $baseUrl = '@web'; + public $css = [ + 'css/site.css', + ]; + public $js = [ + ]; + public $depends = [ + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + ]; } diff --git a/apps/basic/commands/HelloController.php b/apps/basic/commands/HelloController.php index ce567ddaf5d..86ab8b85340 100644 --- a/apps/basic/commands/HelloController.php +++ b/apps/basic/commands/HelloController.php @@ -19,12 +19,12 @@ */ class HelloController extends Controller { - /** - * This command echoes what you have entered as the message. - * @param string $message the message to be echoed. - */ - public function actionIndex($message = 'hello world') - { - echo $message . "\n"; - } + /** + * This command echoes what you have entered as the message. + * @param string $message the message to be echoed. + */ + public function actionIndex($message = 'hello world') + { + echo $message . "\n"; + } } diff --git a/apps/basic/config/console.php b/apps/basic/config/console.php index 1f58e421eb2..f2548585684 100644 --- a/apps/basic/config/console.php +++ b/apps/basic/config/console.php @@ -6,24 +6,24 @@ $db = require(__DIR__ . '/db.php'); return [ - 'id' => 'basic-console', - 'basePath' => dirname(__DIR__), - 'preload' => ['log'], - 'controllerNamespace' => 'app\commands', - 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), - 'components' => [ - 'cache' => [ - 'class' => 'yii\caching\FileCache', - ], - 'log' => [ - 'targets' => [ - [ - 'class' => 'yii\log\FileTarget', - 'levels' => ['error', 'warning'], - ], - ], - ], - 'db' => $db, - ], - 'params' => $params, + 'id' => 'basic-console', + 'basePath' => dirname(__DIR__), + 'preload' => ['log'], + 'controllerNamespace' => 'app\commands', + 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), + 'components' => [ + 'cache' => [ + 'class' => 'yii\caching\FileCache', + ], + 'log' => [ + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + 'db' => $db, + ], + 'params' => $params, ]; diff --git a/apps/basic/config/db.php b/apps/basic/config/db.php index b14e77ea371..c4c12529c1e 100644 --- a/apps/basic/config/db.php +++ b/apps/basic/config/db.php @@ -1,9 +1,9 @@ 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=yii2basic', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8', + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=yii2basic', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', ]; diff --git a/apps/basic/config/params.php b/apps/basic/config/params.php index 93cb368d948..6ebf2792b86 100644 --- a/apps/basic/config/params.php +++ b/apps/basic/config/params.php @@ -1,5 +1,5 @@ 'admin@example.com', + 'adminEmail' => 'admin@example.com', ]; diff --git a/apps/basic/config/web.php b/apps/basic/config/web.php index 6f02f52c710..462c135a886 100644 --- a/apps/basic/config/web.php +++ b/apps/basic/config/web.php @@ -4,43 +4,43 @@ $db = require(__DIR__ . '/db.php'); $config = [ - 'id' => 'basic', - 'basePath' => dirname(__DIR__), - 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), - 'components' => [ - 'cache' => [ - 'class' => 'yii\caching\FileCache', - ], - 'user' => [ - 'identityClass' => 'app\models\User', - 'enableAutoLogin' => true, - ], - 'errorHandler' => [ - 'errorAction' => 'site/error', - ], - 'mail' => [ - 'class' => 'yii\swiftmailer\Mailer', - 'useFileTransport' => true, - ], - 'log' => [ - 'traceLevel' => YII_DEBUG ? 3 : 0, - 'targets' => [ - [ - 'class' => 'yii\log\FileTarget', - 'levels' => ['error', 'warning'], - ], - ], - ], - 'db' => $db, - ], - 'params' => $params, + 'id' => 'basic', + 'basePath' => dirname(__DIR__), + 'extensions' => require(__DIR__ . '/../vendor/yiisoft/extensions.php'), + 'components' => [ + 'cache' => [ + 'class' => 'yii\caching\FileCache', + ], + 'user' => [ + 'identityClass' => 'app\models\User', + 'enableAutoLogin' => true, + ], + 'errorHandler' => [ + 'errorAction' => 'site/error', + ], + 'mail' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'useFileTransport' => true, + ], + 'log' => [ + 'traceLevel' => YII_DEBUG ? 3 : 0, + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], + ], + ], + ], + 'db' => $db, + ], + 'params' => $params, ]; if (YII_ENV_DEV) { - // configuration adjustments for 'dev' environment - $config['preload'][] = 'debug'; - $config['modules']['debug'] = 'yii\debug\Module'; - $config['modules']['gii'] = 'yii\gii\Module'; + // configuration adjustments for 'dev' environment + $config['preload'][] = 'debug'; + $config['modules']['debug'] = 'yii\debug\Module'; + $config['modules']['gii'] = 'yii\gii\Module'; } return $config; diff --git a/apps/basic/controllers/SiteController.php b/apps/basic/controllers/SiteController.php index 0d4d2f9851a..ebecd28e604 100644 --- a/apps/basic/controllers/SiteController.php +++ b/apps/basic/controllers/SiteController.php @@ -11,84 +11,86 @@ class SiteController extends Controller { - public function behaviors() - { - return [ - 'access' => [ - 'class' => AccessControl::className(), - 'only' => ['logout'], - 'rules' => [ - [ - 'actions' => ['logout'], - 'allow' => true, - 'roles' => ['@'], - ], - ], - ], - 'verbs' => [ - 'class' => VerbFilter::className(), - 'actions' => [ - 'logout' => ['post'], - ], - ], - ]; - } + public function behaviors() + { + return [ + 'access' => [ + 'class' => AccessControl::className(), + 'only' => ['logout'], + 'rules' => [ + [ + 'actions' => ['logout'], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'logout' => ['post'], + ], + ], + ]; + } - public function actions() - { - return [ - 'error' => [ - 'class' => 'yii\web\ErrorAction', - ], - 'captcha' => [ - 'class' => 'yii\captcha\CaptchaAction', - 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, - ], - ]; - } + public function actions() + { + return [ + 'error' => [ + 'class' => 'yii\web\ErrorAction', + ], + 'captcha' => [ + 'class' => 'yii\captcha\CaptchaAction', + 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, + ], + ]; + } - public function actionIndex() - { - return $this->render('index'); - } + public function actionIndex() + { + return $this->render('index'); + } - public function actionLogin() - { - if (!\Yii::$app->user->isGuest) { - return $this->goHome(); - } + public function actionLogin() + { + if (!\Yii::$app->user->isGuest) { + return $this->goHome(); + } - $model = new LoginForm(); - if ($model->load(Yii::$app->request->post()) && $model->login()) { - return $this->goBack(); - } else { - return $this->render('login', [ - 'model' => $model, - ]); - } - } + $model = new LoginForm(); + if ($model->load(Yii::$app->request->post()) && $model->login()) { + return $this->goBack(); + } else { + return $this->render('login', [ + 'model' => $model, + ]); + } + } - public function actionLogout() - { - Yii::$app->user->logout(); - return $this->goHome(); - } + public function actionLogout() + { + Yii::$app->user->logout(); - public function actionContact() - { - $model = new ContactForm(); - if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) { - Yii::$app->session->setFlash('contactFormSubmitted'); - return $this->refresh(); - } else { - return $this->render('contact', [ - 'model' => $model, - ]); - } - } + return $this->goHome(); + } - public function actionAbout() - { - return $this->render('about'); - } + public function actionContact() + { + $model = new ContactForm(); + if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) { + Yii::$app->session->setFlash('contactFormSubmitted'); + + return $this->refresh(); + } else { + return $this->render('contact', [ + 'model' => $model, + ]); + } + } + + public function actionAbout() + { + return $this->render('about'); + } } diff --git a/apps/basic/mail/layouts/html.php b/apps/basic/mail/layouts/html.php index 8e2707dc5d1..a9689cc2e5b 100644 --- a/apps/basic/mail/layouts/html.php +++ b/apps/basic/mail/layouts/html.php @@ -10,14 +10,14 @@ - - <?= Html::encode($this->title) ?> - head() ?> + + <?= Html::encode($this->title) ?> + head() ?> - beginBody() ?> - - endBody() ?> + beginBody() ?> + + endBody() ?> -endPage() ?> \ No newline at end of file +endPage() ?> diff --git a/apps/basic/models/ContactForm.php b/apps/basic/models/ContactForm.php index 13445627ee8..29a5dd996c9 100644 --- a/apps/basic/models/ContactForm.php +++ b/apps/basic/models/ContactForm.php @@ -10,54 +10,55 @@ */ class ContactForm extends Model { - public $name; - public $email; - public $subject; - public $body; - public $verifyCode; + public $name; + public $email; + public $subject; + public $body; + public $verifyCode; - /** - * @return array the validation rules. - */ - public function rules() - { - return [ - // name, email, subject and body are required - [['name', 'email', 'subject', 'body'], 'required'], - // email has to be a valid email address - ['email', 'email'], - // verifyCode needs to be entered correctly - ['verifyCode', 'captcha'], - ]; - } + /** + * @return array the validation rules. + */ + public function rules() + { + return [ + // name, email, subject and body are required + [['name', 'email', 'subject', 'body'], 'required'], + // email has to be a valid email address + ['email', 'email'], + // verifyCode needs to be entered correctly + ['verifyCode', 'captcha'], + ]; + } - /** - * @return array customized attribute labels - */ - public function attributeLabels() - { - return [ - 'verifyCode' => 'Verification Code', - ]; - } + /** + * @return array customized attribute labels + */ + public function attributeLabels() + { + return [ + 'verifyCode' => 'Verification Code', + ]; + } - /** - * Sends an email to the specified email address using the information collected by this model. - * @param string $email the target email address - * @return boolean whether the model passes validation - */ - public function contact($email) - { - if ($this->validate()) { - Yii::$app->mail->compose() - ->setTo($email) - ->setFrom([$this->email => $this->name]) - ->setSubject($this->subject) - ->setTextBody($this->body) - ->send(); - return true; - } else { - return false; - } - } + /** + * Sends an email to the specified email address using the information collected by this model. + * @param string $email the target email address + * @return boolean whether the model passes validation + */ + public function contact($email) + { + if ($this->validate()) { + Yii::$app->mail->compose() + ->setTo($email) + ->setFrom([$this->email => $this->name]) + ->setSubject($this->subject) + ->setTextBody($this->body) + ->send(); + + return true; + } else { + return false; + } + } } diff --git a/apps/basic/models/LoginForm.php b/apps/basic/models/LoginForm.php index 76cf1de52e5..31b814e24ee 100644 --- a/apps/basic/models/LoginForm.php +++ b/apps/basic/models/LoginForm.php @@ -10,65 +10,66 @@ */ class LoginForm extends Model { - public $username; - public $password; - public $rememberMe = true; + public $username; + public $password; + public $rememberMe = true; - private $_user = false; + private $_user = false; - /** - * @return array the validation rules. - */ - public function rules() - { - return [ - // username and password are both required - [['username', 'password'], 'required'], - // rememberMe must be a boolean value - ['rememberMe', 'boolean'], - // password is validated by validatePassword() - ['password', 'validatePassword'], - ]; - } + /** + * @return array the validation rules. + */ + public function rules() + { + return [ + // username and password are both required + [['username', 'password'], 'required'], + // rememberMe must be a boolean value + ['rememberMe', 'boolean'], + // password is validated by validatePassword() + ['password', 'validatePassword'], + ]; + } - /** - * Validates the password. - * This method serves as the inline validation for password. - */ - public function validatePassword() - { - if (!$this->hasErrors()) { - $user = $this->getUser(); + /** + * Validates the password. + * This method serves as the inline validation for password. + */ + public function validatePassword() + { + if (!$this->hasErrors()) { + $user = $this->getUser(); - if (!$user || !$user->validatePassword($this->password)) { - $this->addError('password', 'Incorrect username or password.'); - } - } - } + if (!$user || !$user->validatePassword($this->password)) { + $this->addError('password', 'Incorrect username or password.'); + } + } + } - /** - * Logs in a user using the provided username and password. - * @return boolean whether the user is logged in successfully - */ - public function login() - { - if ($this->validate()) { - return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0); - } else { - return false; - } - } + /** + * Logs in a user using the provided username and password. + * @return boolean whether the user is logged in successfully + */ + public function login() + { + if ($this->validate()) { + return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0); + } else { + return false; + } + } - /** - * Finds user by [[username]] - * - * @return User|null - */ - public function getUser() - { - if ($this->_user === false) { - $this->_user = User::findByUsername($this->username); - } - return $this->_user; - } + /** + * Finds user by [[username]] + * + * @return User|null + */ + public function getUser() + { + if ($this->_user === false) { + $this->_user = User::findByUsername($this->username); + } + + return $this->_user; + } } diff --git a/apps/basic/models/User.php b/apps/basic/models/User.php index fd0d0d434a4..5de7e20c2ad 100644 --- a/apps/basic/models/User.php +++ b/apps/basic/models/User.php @@ -4,98 +4,100 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface { - public $id; - public $username; - public $password; - public $authKey; - public $accessToken; + public $id; + public $username; + public $password; + public $authKey; + public $accessToken; - private static $users = [ - '100' => [ - 'id' => '100', - 'username' => 'admin', - 'password' => 'admin', - 'authKey' => 'test100key', - 'accessToken' => '100-token', - ], - '101' => [ - 'id' => '101', - 'username' => 'demo', - 'password' => 'demo', - 'authKey' => 'test101key', - 'accessToken' => '101-token', - ], - ]; + private static $users = [ + '100' => [ + 'id' => '100', + 'username' => 'admin', + 'password' => 'admin', + 'authKey' => 'test100key', + 'accessToken' => '100-token', + ], + '101' => [ + 'id' => '101', + 'username' => 'demo', + 'password' => 'demo', + 'authKey' => 'test101key', + 'accessToken' => '101-token', + ], + ]; - /** - * @inheritdoc - */ - public static function findIdentity($id) - { - return isset(self::$users[$id]) ? new static(self::$users[$id]) : null; - } + /** + * @inheritdoc + */ + public static function findIdentity($id) + { + return isset(self::$users[$id]) ? new static(self::$users[$id]) : null; + } - /** - * @inheritdoc - */ - public static function findIdentityByAccessToken($token) - { - foreach (self::$users as $user) { - if ($user['accessToken'] === $token) { - return new static($user); - } - } - return null; - } + /** + * @inheritdoc + */ + public static function findIdentityByAccessToken($token) + { + foreach (self::$users as $user) { + if ($user['accessToken'] === $token) { + return new static($user); + } + } - /** - * Finds user by username - * - * @param string $username - * @return static|null - */ - public static function findByUsername($username) - { - foreach (self::$users as $user) { - if (strcasecmp($user['username'], $username) === 0) { - return new static($user); - } - } - return null; - } + return null; + } - /** - * @inheritdoc - */ - public function getId() - { - return $this->id; - } + /** + * Finds user by username + * + * @param string $username + * @return static|null + */ + public static function findByUsername($username) + { + foreach (self::$users as $user) { + if (strcasecmp($user['username'], $username) === 0) { + return new static($user); + } + } - /** - * @inheritdoc - */ - public function getAuthKey() - { - return $this->authKey; - } + return null; + } - /** - * @inheritdoc - */ - public function validateAuthKey($authKey) - { - return $this->authKey === $authKey; - } + /** + * @inheritdoc + */ + public function getId() + { + return $this->id; + } - /** - * Validates password - * - * @param string $password password to validate - * @return boolean if password provided is valid for current user - */ - public function validatePassword($password) - { - return $this->password === $password; - } + /** + * @inheritdoc + */ + public function getAuthKey() + { + return $this->authKey; + } + + /** + * @inheritdoc + */ + public function validateAuthKey($authKey) + { + return $this->authKey === $authKey; + } + + /** + * Validates password + * + * @param string $password password to validate + * @return boolean if password provided is valid for current user + */ + public function validatePassword($password) + { + return $this->password === $password; + } } diff --git a/apps/basic/requirements.php b/apps/basic/requirements.php index d470f99d7be..b96e4d69dcb 100644 --- a/apps/basic/requirements.php +++ b/apps/basic/requirements.php @@ -14,10 +14,10 @@ $frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2'; if (!is_dir($frameworkPath)) { - echo '

Error

'; - echo '

The path to yii framework seems to be incorrect.

'; - echo '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . '.

'; - echo '

Please refer to the README on how to install Yii.

'; + echo '

Error

'; + echo '

The path to yii framework seems to be incorrect.

'; + echo '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . '.

'; + echo '

Please refer to the README on how to install Yii.

'; } require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); @@ -27,84 +27,84 @@ * Adjust requirements according to your application specifics. */ $requirements = array( - // Database : - array( - 'name' => 'PDO extension', - 'mandatory' => true, - 'condition' => extension_loaded('pdo'), - 'by' => 'All DB-related classes', - ), - array( - 'name' => 'PDO SQLite extension', - 'mandatory' => false, - 'condition' => extension_loaded('pdo_sqlite'), - 'by' => 'All DB-related classes', - 'memo' => 'Required for SQLite database.', - ), - array( - 'name' => 'PDO MySQL extension', - 'mandatory' => false, - 'condition' => extension_loaded('pdo_mysql'), - 'by' => 'All DB-related classes', - 'memo' => 'Required for MySQL database.', - ), - array( - 'name' => 'PDO PostgreSQL extension', - 'mandatory' => false, - 'condition' => extension_loaded('pdo_pgsql'), - 'by' => 'All DB-related classes', - 'memo' => 'Required for PostgreSQL database.', - ), - // Cache : - array( - 'name' => 'Memcache extension', - 'mandatory' => false, - 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), - 'by' => 'CMemCache', - 'memo' => extension_loaded('memcached') ? 'To use memcached set CMemCache::useMemcached to true.' : '' - ), - array( - 'name' => 'APC extension', - 'mandatory' => false, - 'condition' => extension_loaded('apc'), - 'by' => 'CApcCache', - ), - // Additional PHP extensions : - array( - 'name' => 'Mcrypt extension', - 'mandatory' => false, - 'condition' => extension_loaded('mcrypt'), - 'by' => 'CSecurityManager', - 'memo' => 'Required by encrypt and decrypt methods.' - ), - // PHP ini : - 'phpSafeMode' => array( - 'name' => 'PHP safe mode', - 'mandatory' => false, - 'condition' => $requirementsChecker->checkPhpIniOff("safe_mode"), - 'by' => 'File uploading and console command execution', - 'memo' => '"safe_mode" should be disabled at php.ini', - ), - 'phpExposePhp' => array( - 'name' => 'Expose PHP', - 'mandatory' => false, - 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), - 'by' => 'Security reasons', - 'memo' => '"expose_php" should be disabled at php.ini', - ), - 'phpAllowUrlInclude' => array( - 'name' => 'PHP allow url include', - 'mandatory' => false, - 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), - 'by' => 'Security reasons', - 'memo' => '"allow_url_include" should be disabled at php.ini', - ), - 'phpSmtp' => array( - 'name' => 'PHP mail SMTP', - 'mandatory' => false, - 'condition' => strlen(ini_get('SMTP'))>0, - 'by' => 'Email sending', - 'memo' => 'PHP mail SMTP server required', - ), + // Database : + array( + 'name' => 'PDO extension', + 'mandatory' => true, + 'condition' => extension_loaded('pdo'), + 'by' => 'All DB-related classes', + ), + array( + 'name' => 'PDO SQLite extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_sqlite'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for SQLite database.', + ), + array( + 'name' => 'PDO MySQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_mysql'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for MySQL database.', + ), + array( + 'name' => 'PDO PostgreSQL extension', + 'mandatory' => false, + 'condition' => extension_loaded('pdo_pgsql'), + 'by' => 'All DB-related classes', + 'memo' => 'Required for PostgreSQL database.', + ), + // Cache : + array( + 'name' => 'Memcache extension', + 'mandatory' => false, + 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), + 'by' => 'CMemCache', + 'memo' => extension_loaded('memcached') ? 'To use memcached set CMemCache::useMemcached to true.' : '' + ), + array( + 'name' => 'APC extension', + 'mandatory' => false, + 'condition' => extension_loaded('apc'), + 'by' => 'CApcCache', + ), + // Additional PHP extensions : + array( + 'name' => 'Mcrypt extension', + 'mandatory' => false, + 'condition' => extension_loaded('mcrypt'), + 'by' => 'CSecurityManager', + 'memo' => 'Required by encrypt and decrypt methods.' + ), + // PHP ini : + 'phpSafeMode' => array( + 'name' => 'PHP safe mode', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("safe_mode"), + 'by' => 'File uploading and console command execution', + 'memo' => '"safe_mode" should be disabled at php.ini', + ), + 'phpExposePhp' => array( + 'name' => 'Expose PHP', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), + 'by' => 'Security reasons', + 'memo' => '"expose_php" should be disabled at php.ini', + ), + 'phpAllowUrlInclude' => array( + 'name' => 'PHP allow url include', + 'mandatory' => false, + 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), + 'by' => 'Security reasons', + 'memo' => '"allow_url_include" should be disabled at php.ini', + ), + 'phpSmtp' => array( + 'name' => 'PHP mail SMTP', + 'mandatory' => false, + 'condition' => strlen(ini_get('SMTP'))>0, + 'by' => 'Email sending', + 'memo' => 'PHP mail SMTP server required', + ), ); $requirementsChecker->checkYii()->check($requirements)->render(); diff --git a/apps/basic/tests/_config.php b/apps/basic/tests/_config.php index d9cc356dd80..24ec1b56aec 100644 --- a/apps/basic/tests/_config.php +++ b/apps/basic/tests/_config.php @@ -3,12 +3,12 @@ * application configurations shared by all test types */ return [ - 'components' => [ - 'mail' => [ - 'useFileTransport' => true, - ], - 'urlManager' => [ - 'showScriptName' => true, - ], - ], + 'components' => [ + 'mail' => [ + 'useFileTransport' => true, + ], + 'urlManager' => [ + 'showScriptName' => true, + ], + ], ]; diff --git a/apps/basic/tests/_helpers/CodeHelper.php b/apps/basic/tests/_helpers/CodeHelper.php index 64484868d87..201503ddfeb 100644 --- a/apps/basic/tests/_helpers/CodeHelper.php +++ b/apps/basic/tests/_helpers/CodeHelper.php @@ -3,5 +3,5 @@ class CodeHelper extends \Codeception\Module { - // here you can define custom methods for CodeGuy + // here you can define custom methods for CodeGuy } diff --git a/apps/basic/tests/_helpers/TestHelper.php b/apps/basic/tests/_helpers/TestHelper.php index 9558ff151bf..ae9418775b5 100644 --- a/apps/basic/tests/_helpers/TestHelper.php +++ b/apps/basic/tests/_helpers/TestHelper.php @@ -3,5 +3,5 @@ class TestHelper extends \Codeception\Module { - // here you can define custom methods for TestGuy + // here you can define custom methods for TestGuy } diff --git a/apps/basic/tests/_helpers/WebHelper.php b/apps/basic/tests/_helpers/WebHelper.php index b2953af3a74..a8c72502ad7 100644 --- a/apps/basic/tests/_helpers/WebHelper.php +++ b/apps/basic/tests/_helpers/WebHelper.php @@ -3,5 +3,5 @@ class WebHelper extends \Codeception\Module { - // here you can define custom methods for WebGuy + // here you can define custom methods for WebGuy } diff --git a/apps/basic/tests/_pages/AboutPage.php b/apps/basic/tests/_pages/AboutPage.php index 5f9021f4e68..77acdb81592 100644 --- a/apps/basic/tests/_pages/AboutPage.php +++ b/apps/basic/tests/_pages/AboutPage.php @@ -6,5 +6,5 @@ class AboutPage extends BasePage { - public $route = 'site/about'; + public $route = 'site/about'; } diff --git a/apps/basic/tests/_pages/ContactPage.php b/apps/basic/tests/_pages/ContactPage.php index 18015b57c6f..24c8dfa567d 100644 --- a/apps/basic/tests/_pages/ContactPage.php +++ b/apps/basic/tests/_pages/ContactPage.php @@ -6,17 +6,17 @@ class ContactPage extends BasePage { - public $route = 'site/contact'; + public $route = 'site/contact'; - /** - * @param array $contactData - */ - public function submit(array $contactData) - { - foreach ($contactData as $field => $value) { - $inputType = $field === 'body' ? 'textarea' : 'input'; - $this->guy->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value); - } - $this->guy->click('contact-button'); - } + /** + * @param array $contactData + */ + public function submit(array $contactData) + { + foreach ($contactData as $field => $value) { + $inputType = $field === 'body' ? 'textarea' : 'input'; + $this->guy->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value); + } + $this->guy->click('contact-button'); + } } diff --git a/apps/basic/tests/_pages/LoginPage.php b/apps/basic/tests/_pages/LoginPage.php index 503a4205f1b..c73d729affe 100644 --- a/apps/basic/tests/_pages/LoginPage.php +++ b/apps/basic/tests/_pages/LoginPage.php @@ -6,16 +6,16 @@ class LoginPage extends BasePage { - public $route = 'site/login'; + public $route = 'site/login'; - /** - * @param string $username - * @param string $password - */ - public function login($username, $password) - { - $this->guy->fillField('input[name="LoginForm[username]"]', $username); - $this->guy->fillField('input[name="LoginForm[password]"]', $password); - $this->guy->click('login-button'); - } + /** + * @param string $username + * @param string $password + */ + public function login($username, $password) + { + $this->guy->fillField('input[name="LoginForm[username]"]', $username); + $this->guy->fillField('input[name="LoginForm[password]"]', $password); + $this->guy->click('login-button'); + } } diff --git a/apps/basic/tests/acceptance.suite.yml b/apps/basic/tests/acceptance.suite.yml index b3852841d41..2e35aa558d1 100644 --- a/apps/basic/tests/acceptance.suite.yml +++ b/apps/basic/tests/acceptance.suite.yml @@ -15,7 +15,7 @@ modules: - PhpBrowser # you can use WebDriver instead of PhpBrowser to test javascript and ajax. # This will require you to install selenium. See http://codeception.com/docs/04-AcceptanceTests#Selenium -# "restart" option is used by the WebDriver to start each time per test-file new session and cookies, +# "restart" option is used by the WebDriver to start each time per test-file new session and cookies, # it is useful if you want to login in your app in each test. # - WebDriver config: diff --git a/apps/basic/tests/acceptance/ContactCept.php b/apps/basic/tests/acceptance/ContactCept.php index 25f5735e4a5..e76ac9beb74 100644 --- a/apps/basic/tests/acceptance/ContactCept.php +++ b/apps/basic/tests/acceptance/ContactCept.php @@ -21,11 +21,11 @@ $I->amGoingTo('submit contact form with not correct email'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester.email', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester.email', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->expectTo('see that email adress is wrong'); $I->dontSee('Name cannot be blank', '.help-inline'); @@ -36,14 +36,14 @@ $I->amGoingTo('submit contact form with correct data'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester@example.com', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester@example.com', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); if (method_exists($I, 'wait')) { - $I->wait(3); // only for selenium + $I->wait(3); // only for selenium } $I->dontSeeElement('#contact-form'); $I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/basic/tests/acceptance/LoginCept.php b/apps/basic/tests/acceptance/LoginCept.php index 5d6a3878e52..07d1c452e62 100644 --- a/apps/basic/tests/acceptance/LoginCept.php +++ b/apps/basic/tests/acceptance/LoginCept.php @@ -23,7 +23,7 @@ $I->amGoingTo('try to login with correct credentials'); $loginPage->login('admin', 'admin'); if (method_exists($I, 'wait')) { - $I->wait(3); // only for selenium + $I->wait(3); // only for selenium } $I->expectTo('see user info'); $I->see('Logout (admin)'); diff --git a/apps/basic/tests/acceptance/_config.php b/apps/basic/tests/acceptance/_config.php index 4c306b3bb77..857a804f49f 100644 --- a/apps/basic/tests/acceptance/_config.php +++ b/apps/basic/tests/acceptance/_config.php @@ -1,13 +1,13 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_acceptance', - ], - ], - ] + require(__DIR__ . '/../../config/web.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_acceptance', + ], + ], + ] ); diff --git a/apps/basic/tests/acceptance/_console.php b/apps/basic/tests/acceptance/_console.php index f89eecfc20f..03cdd1ff6f9 100644 --- a/apps/basic/tests/acceptance/_console.php +++ b/apps/basic/tests/acceptance/_console.php @@ -1,13 +1,13 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_acceptance', - ], - ], - ] + require(__DIR__ . '/../../config/console.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_acceptance', + ], + ], + ] ); diff --git a/apps/basic/tests/functional/ContactCept.php b/apps/basic/tests/functional/ContactCept.php index 14e61973eb8..49d7735fd71 100644 --- a/apps/basic/tests/functional/ContactCept.php +++ b/apps/basic/tests/functional/ContactCept.php @@ -21,11 +21,11 @@ $I->amGoingTo('submit contact form with not correct email'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester.email', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester.email', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->expectTo('see that email adress is wrong'); $I->dontSee('Name cannot be blank', '.help-inline'); @@ -36,11 +36,11 @@ $I->amGoingTo('submit contact form with correct data'); $contactPage->submit([ - 'name' => 'tester', - 'email' => 'tester@example.com', - 'subject' => 'test subject', - 'body' => 'test content', - 'verifyCode' => 'testme', + 'name' => 'tester', + 'email' => 'tester@example.com', + 'subject' => 'test subject', + 'body' => 'test content', + 'verifyCode' => 'testme', ]); $I->dontSeeElement('#contact-form'); $I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/basic/tests/functional/_config.php b/apps/basic/tests/functional/_config.php index c2ecae0ea0b..512e802bb2d 100644 --- a/apps/basic/tests/functional/_config.php +++ b/apps/basic/tests/functional/_config.php @@ -5,13 +5,13 @@ $_SERVER['SCRIPT_NAME'] = TEST_ENTRY_URL; return yii\helpers\ArrayHelper::merge( - require(__DIR__ . '/../../config/web.php'), - require(__DIR__ . '/../_config.php'), - [ - 'components' => [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_functional', - ], - ], - ] + require(__DIR__ . '/../../config/web.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_functional', + ], + ], + ] ); diff --git a/apps/basic/tests/functional/_console.php b/apps/basic/tests/functional/_console.php index 2e674c4f970..abeee696c61 100644 --- a/apps/basic/tests/functional/_console.php +++ b/apps/basic/tests/functional/_console.php @@ -1,13 +1,13 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_functional', - ], - ], - ] + require(__DIR__ . '/../../config/console.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_functional', + ], + ], + ] ); diff --git a/apps/basic/tests/unit/_config.php b/apps/basic/tests/unit/_config.php index 45f1ef42fbc..2559ef31e08 100644 --- a/apps/basic/tests/unit/_config.php +++ b/apps/basic/tests/unit/_config.php @@ -1,13 +1,13 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_unit', - ], - ], - ] + require(__DIR__ . '/../../config/web.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_unit', + ], + ], + ] ); diff --git a/apps/basic/tests/unit/_console.php b/apps/basic/tests/unit/_console.php index d2a233ab306..04272a3216a 100644 --- a/apps/basic/tests/unit/_console.php +++ b/apps/basic/tests/unit/_console.php @@ -1,13 +1,13 @@ [ - 'db' => [ - 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_unit', - ], - ], - ] + require(__DIR__ . '/../../config/console.php'), + require(__DIR__ . '/../_config.php'), + [ + 'components' => [ + 'db' => [ + 'dsn' => 'mysql:host=localhost;dbname=yii2_basic_unit', + ], + ], + ] ); diff --git a/apps/basic/tests/unit/models/ContactFormTest.php b/apps/basic/tests/unit/models/ContactFormTest.php index c6797178ed7..d8a55ee83e1 100644 --- a/apps/basic/tests/unit/models/ContactFormTest.php +++ b/apps/basic/tests/unit/models/ContactFormTest.php @@ -7,52 +7,52 @@ class ContactFormTest extends TestCase { - use \Codeception\Specify; - - protected function setUp() - { - parent::setUp(); - Yii::$app->mail->fileTransportCallback = function ($mailer, $message) { - return 'testing_message.eml'; - }; - } - - protected function tearDown() - { - unlink($this->getMessageFile()); - parent::tearDown(); - } - - public function testContact() - { - $model = $this->getMock('app\models\ContactForm', ['validate']); - $model->expects($this->once())->method('validate')->will($this->returnValue(true)); - - $model->attributes = [ - 'name' => 'Tester', - 'email' => 'tester@example.com', - 'subject' => 'very important letter subject', - 'body' => 'body of current message', - ]; - - $model->contact('admin@example.com'); - - $this->specify('email should be send', function () { - expect('email file should exist', file_exists($this->getMessageFile()))->true(); - }); - - $this->specify('message should contain correct data', function () use ($model) { - $emailMessage = file_get_contents($this->getMessageFile()); - - expect('email should contain user name', $emailMessage)->contains($model->name); - expect('email should contain sender email', $emailMessage)->contains($model->email); - expect('email should contain subject', $emailMessage)->contains($model->subject); - expect('email should contain body', $emailMessage)->contains($model->body); - }); - } - - private function getMessageFile() - { - return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; - } + use \Codeception\Specify; + + protected function setUp() + { + parent::setUp(); + Yii::$app->mail->fileTransportCallback = function ($mailer, $message) { + return 'testing_message.eml'; + }; + } + + protected function tearDown() + { + unlink($this->getMessageFile()); + parent::tearDown(); + } + + public function testContact() + { + $model = $this->getMock('app\models\ContactForm', ['validate']); + $model->expects($this->once())->method('validate')->will($this->returnValue(true)); + + $model->attributes = [ + 'name' => 'Tester', + 'email' => 'tester@example.com', + 'subject' => 'very important letter subject', + 'body' => 'body of current message', + ]; + + $model->contact('admin@example.com'); + + $this->specify('email should be send', function () { + expect('email file should exist', file_exists($this->getMessageFile()))->true(); + }); + + $this->specify('message should contain correct data', function () use ($model) { + $emailMessage = file_get_contents($this->getMessageFile()); + + expect('email should contain user name', $emailMessage)->contains($model->name); + expect('email should contain sender email', $emailMessage)->contains($model->email); + expect('email should contain subject', $emailMessage)->contains($model->subject); + expect('email should contain body', $emailMessage)->contains($model->body); + }); + } + + private function getMessageFile() + { + return Yii::getAlias(Yii::$app->mail->fileTransportPath) . '/testing_message.eml'; + } } diff --git a/apps/basic/tests/unit/models/LoginFormTest.php b/apps/basic/tests/unit/models/LoginFormTest.php index d1f6043faf4..c2c28cc5183 100644 --- a/apps/basic/tests/unit/models/LoginFormTest.php +++ b/apps/basic/tests/unit/models/LoginFormTest.php @@ -8,59 +8,60 @@ class LoginFormTest extends TestCase { - use \Codeception\Specify; + use \Codeception\Specify; - protected function tearDown() - { - Yii::$app->user->logout(); - parent::tearDown(); - } + protected function tearDown() + { + Yii::$app->user->logout(); + parent::tearDown(); + } - public function testLoginNoUser() - { - $model = $this->mockUser(null); + public function testLoginNoUser() + { + $model = $this->mockUser(null); - $model->username = 'some_username'; - $model->password = 'some_password'; + $model->username = 'some_username'; + $model->password = 'some_password'; - $this->specify('user should not be able to login, when there is no identity', function () use ($model) { - expect('model should not login user', $model->login())->false(); - expect('user should not be logged in', Yii::$app->user->isGuest)->true(); - }); - } + $this->specify('user should not be able to login, when there is no identity', function () use ($model) { + expect('model should not login user', $model->login())->false(); + expect('user should not be logged in', Yii::$app->user->isGuest)->true(); + }); + } - public function testLoginWrongPassword() - { - $model = $this->mockUser(new User); + public function testLoginWrongPassword() + { + $model = $this->mockUser(new User); - $model->username = 'demo'; - $model->password = 'wrong-password'; + $model->username = 'demo'; + $model->password = 'wrong-password'; - $this->specify('user should not be able to login with wrong password', function () use ($model) { - expect('model should not login user', $model->login())->false(); - expect('error message should be set', $model->errors)->hasKey('password'); - expect('user should not be logged in', Yii::$app->user->isGuest)->true(); - }); - } + $this->specify('user should not be able to login with wrong password', function () use ($model) { + expect('model should not login user', $model->login())->false(); + expect('error message should be set', $model->errors)->hasKey('password'); + expect('user should not be logged in', Yii::$app->user->isGuest)->true(); + }); + } - public function testLoginCorrect() - { - $model = $this->mockUser(new User(['password' => 'demo'])); + public function testLoginCorrect() + { + $model = $this->mockUser(new User(['password' => 'demo'])); - $model->username = 'demo'; - $model->password = 'demo'; + $model->username = 'demo'; + $model->password = 'demo'; - $this->specify('user should be able to login with correct credentials', function () use ($model) { - expect('model should login user', $model->login())->true(); - expect('error message should not be set', $model->errors)->hasntKey('password'); - expect('user should be logged in', Yii::$app->user->isGuest)->false(); - }); - } + $this->specify('user should be able to login with correct credentials', function () use ($model) { + expect('model should login user', $model->login())->true(); + expect('error message should not be set', $model->errors)->hasntKey('password'); + expect('user should be logged in', Yii::$app->user->isGuest)->false(); + }); + } - private function mockUser($user) - { - $loginForm = $this->getMock('app\models\LoginForm', ['getUser']); - $loginForm->expects($this->any())->method('getUser')->will($this->returnValue($user)); - return $loginForm; - } + private function mockUser($user) + { + $loginForm = $this->getMock('app\models\LoginForm', ['getUser']); + $loginForm->expects($this->any())->method('getUser')->will($this->returnValue($user)); + + return $loginForm; + } } diff --git a/apps/basic/tests/unit/models/UserTest.php b/apps/basic/tests/unit/models/UserTest.php index 109da730d36..8cd92153311 100644 --- a/apps/basic/tests/unit/models/UserTest.php +++ b/apps/basic/tests/unit/models/UserTest.php @@ -6,12 +6,12 @@ class UserTest extends TestCase { - protected function setUp() - { - parent::setUp(); - // uncomment the following to load fixtures for table tbl_user - //$this->loadFixtures(['tbl_user']); - } + protected function setUp() + { + parent::setUp(); + // uncomment the following to load fixtures for table tbl_user + //$this->loadFixtures(['tbl_user']); + } - // TODO add test methods here + // TODO add test methods here } diff --git a/apps/basic/views/layouts/main.php b/apps/basic/views/layouts/main.php index f32b53fe804..6af9564ab3d 100644 --- a/apps/basic/views/layouts/main.php +++ b/apps/basic/views/layouts/main.php @@ -15,53 +15,53 @@ - - - <?= Html::encode($this->title) ?> - head() ?> + + + <?= Html::encode($this->title) ?> + head() ?> beginBody() ?> -
- 'My Company', - 'brandUrl' => Yii::$app->homeUrl, - 'options' => [ - 'class' => 'navbar-inverse navbar-fixed-top', - ], - ]); - echo Nav::widget([ - 'options' => ['class' => 'navbar-nav navbar-right'], - 'items' => [ - ['label' => 'Home', 'url' => ['/site/index']], - ['label' => 'About', 'url' => ['/site/about']], - ['label' => 'Contact', 'url' => ['/site/contact']], - Yii::$app->user->isGuest ? - ['label' => 'Login', 'url' => ['/site/login']] : - ['label' => 'Logout (' . Yii::$app->user->identity->username . ')', - 'url' => ['/site/logout'], - 'linkOptions' => ['data-method' => 'post']], - ], - ]); - NavBar::end(); - ?> +
+ 'My Company', + 'brandUrl' => Yii::$app->homeUrl, + 'options' => [ + 'class' => 'navbar-inverse navbar-fixed-top', + ], + ]); + echo Nav::widget([ + 'options' => ['class' => 'navbar-nav navbar-right'], + 'items' => [ + ['label' => 'Home', 'url' => ['/site/index']], + ['label' => 'About', 'url' => ['/site/about']], + ['label' => 'Contact', 'url' => ['/site/contact']], + Yii::$app->user->isGuest ? + ['label' => 'Login', 'url' => ['/site/login']] : + ['label' => 'Logout (' . Yii::$app->user->identity->username . ')', + 'url' => ['/site/logout'], + 'linkOptions' => ['data-method' => 'post']], + ], + ]); + NavBar::end(); + ?> -
- isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], - ]) ?> - -
-
+
+ isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], + ]) ?> + +
+
- + endBody() ?> diff --git a/apps/basic/views/site/about.php b/apps/basic/views/site/about.php index 0608dd2ec54..58969ab7ac1 100644 --- a/apps/basic/views/site/about.php +++ b/apps/basic/views/site/about.php @@ -8,11 +8,11 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

- This is the About page. You may modify the following file to customize its content: -

+

+ This is the About page. You may modify the following file to customize its content: +

- +
diff --git a/apps/basic/views/site/contact.php b/apps/basic/views/site/contact.php index bcaf542a729..6d70462eb1d 100644 --- a/apps/basic/views/site/contact.php +++ b/apps/basic/views/site/contact.php @@ -12,47 +12,47 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

- - session->hasFlash('contactFormSubmitted')): ?> - -
- Thank you for contacting us. We will respond to you as soon as possible. -
- -

- Note that if you turn on the Yii debugger, you should be able - to view the mail message on the mail panel of the debugger. - mail->useFileTransport): ?> - Because the application is in development mode, the email is not sent but saved as - a file under mail->fileTransportPath) ?>. - Please configure the useFileTransport property of the mail - application component to be false to enable email sending. - -

- - - -

- If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. -

- -
-
- 'contact-form']); ?> - field($model, 'name') ?> - field($model, 'email') ?> - field($model, 'subject') ?> - field($model, 'body')->textArea(['rows' => 6]) ?> - field($model, 'verifyCode')->widget(Captcha::className(), [ - 'template' => '
{image}
{input}
', - ]) ?> -
- 'btn btn-primary', 'name' => 'contact-button']) ?> -
- -
-
- - +

title) ?>

+ + session->hasFlash('contactFormSubmitted')): ?> + +
+ Thank you for contacting us. We will respond to you as soon as possible. +
+ +

+ Note that if you turn on the Yii debugger, you should be able + to view the mail message on the mail panel of the debugger. + mail->useFileTransport): ?> + Because the application is in development mode, the email is not sent but saved as + a file under mail->fileTransportPath) ?>. + Please configure the useFileTransport property of the mail + application component to be false to enable email sending. + +

+ + + +

+ If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. +

+ +
+
+ 'contact-form']); ?> + field($model, 'name') ?> + field($model, 'email') ?> + field($model, 'subject') ?> + field($model, 'body')->textArea(['rows' => 6]) ?> + field($model, 'verifyCode')->widget(Captcha::className(), [ + 'template' => '
{image}
{input}
', + ]) ?> +
+ 'btn btn-primary', 'name' => 'contact-button']) ?> +
+ +
+
+ +
diff --git a/apps/basic/views/site/error.php b/apps/basic/views/site/error.php index 1b7ce0422e6..c172fd621a3 100644 --- a/apps/basic/views/site/error.php +++ b/apps/basic/views/site/error.php @@ -13,17 +13,17 @@ ?>
-

title) ?>

+

title) ?>

-
- -
+
+ +
-

- The above error occurred while the Web server was processing your request. -

-

- Please contact us if you think this is a server error. Thank you. -

+

+ The above error occurred while the Web server was processing your request. +

+

+ Please contact us if you think this is a server error. Thank you. +

diff --git a/apps/basic/views/site/index.php b/apps/basic/views/site/index.php index bcb22781a60..6b6394e5626 100644 --- a/apps/basic/views/site/index.php +++ b/apps/basic/views/site/index.php @@ -6,48 +6,48 @@ ?>
-
-

Congratulations!

+
+

Congratulations!

-

You have successfully created your Yii-powered application.

+

You have successfully created your Yii-powered application.

-

Get started with Yii

-
+

Get started with Yii

+
-
+
-
-
-

Heading

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Documentation »

-
-
-

Heading

+

Yii Documentation »

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Forum »

-
-
-

Heading

+

Yii Forum »

+
+
+

Heading

-

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

+

Lorem ipsum dolor sit amet, consectetur adipisicing 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur.

-

Yii Extensions »

-
-
+

Yii Extensions »

+
+
-
+
diff --git a/apps/basic/views/site/login.php b/apps/basic/views/site/login.php index 7d14c54ddb3..80cc5485e48 100644 --- a/apps/basic/views/site/login.php +++ b/apps/basic/views/site/login.php @@ -11,37 +11,37 @@ $this->params['breadcrumbs'][] = $this->title; ?>
-

title) ?>

+

title) ?>

-

Please fill out the following fields to login:

+

Please fill out the following fields to login:

- 'login-form', - 'options' => ['class' => 'form-horizontal'], - 'fieldConfig' => [ - 'template' => "{label}\n
{input}
\n
{error}
", - 'labelOptions' => ['class' => 'col-lg-1 control-label'], - ], - ]); ?> + 'login-form', + 'options' => ['class' => 'form-horizontal'], + 'fieldConfig' => [ + 'template' => "{label}\n
{input}
\n
{error}
", + 'labelOptions' => ['class' => 'col-lg-1 control-label'], + ], + ]); ?> - field($model, 'username') ?> + field($model, 'username') ?> - field($model, 'password')->passwordInput() ?> + field($model, 'password')->passwordInput() ?> - field($model, 'rememberMe', [ - 'template' => "
{input}
\n
{error}
", - ])->checkbox() ?> + field($model, 'rememberMe', [ + 'template' => "
{input}
\n
{error}
", + ])->checkbox() ?> -
-
- 'btn btn-primary', 'name' => 'login-button']) ?> -
-
+
+
+ 'btn btn-primary', 'name' => 'login-button']) ?> +
+
- + -
- You may login with admin/admin or demo/demo.
- To modify the username/password, please check out the code app\models\User::$users. -
+
+ You may login with admin/admin or demo/demo.
+ To modify the username/password, please check out the code app\models\User::$users. +
diff --git a/apps/basic/web/index-test.php b/apps/basic/web/index-test.php index b5ed6956f5a..326608d21c2 100644 --- a/apps/basic/web/index-test.php +++ b/apps/basic/web/index-test.php @@ -2,7 +2,7 @@ // NOTE: Make sure this file is not accessible when deployed to production if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { - die('You are not allowed to access this file.'); + die('You are not allowed to access this file.'); } defined('YII_DEBUG') or define('YII_DEBUG', true); diff --git a/apps/benchmark/index.php b/apps/benchmark/index.php index 2bcb2524148..c278f1d7e0f 100644 --- a/apps/benchmark/index.php +++ b/apps/benchmark/index.php @@ -5,13 +5,13 @@ require(__DIR__ . '/protected/vendor/yiisoft/yii2/Yii.php'); $config = [ - 'id' => 'benchmark', - 'basePath' => __DIR__ . '/protected', - 'components' => [ - 'urlManager' => [ - 'enablePrettyUrl' => true, - ], - ], + 'id' => 'benchmark', + 'basePath' => __DIR__ . '/protected', + 'components' => [ + 'urlManager' => [ + 'enablePrettyUrl' => true, + ], + ], ]; $application = new yii\web\Application($config); diff --git a/apps/benchmark/protected/controllers/SiteController.php b/apps/benchmark/protected/controllers/SiteController.php index 9abc164f392..9b08da87114 100644 --- a/apps/benchmark/protected/controllers/SiteController.php +++ b/apps/benchmark/protected/controllers/SiteController.php @@ -6,10 +6,10 @@ class SiteController extends Controller { - public $defaultAction = 'hello'; + public $defaultAction = 'hello'; - public function actionHello() - { - return 'hello world'; - } + public function actionHello() + { + return 'hello world'; + } } diff --git a/extensions/apidoc/commands/ApiController.php b/extensions/apidoc/commands/ApiController.php index aeeea44356e..fb16078b4c8 100644 --- a/extensions/apidoc/commands/ApiController.php +++ b/extensions/apidoc/commands/ApiController.php @@ -23,140 +23,144 @@ */ class ApiController extends BaseController { - /** - * @var string url to where the guide files are located - */ - public $guide; - - // TODO add force update option - - /** - * Renders API documentation files - * @param array $sourceDirs - * @param string $targetDir - * @return int - */ - public function actionIndex(array $sourceDirs, $targetDir) - { - $renderer = $this->findRenderer($this->template); - $targetDir = $this->normalizeTargetDir($targetDir); - if ($targetDir === false || $renderer === false) { - return 1; - } - - $renderer->apiUrl = './'; - - // setup reference to guide - if ($this->guide !== null) { - $guideUrl = $this->guide; - $referenceFile = $guideUrl . '/' . BaseRenderer::GUIDE_PREFIX . 'references.txt'; - } else { - $guideUrl = './'; - $referenceFile = $targetDir . '/' . BaseRenderer::GUIDE_PREFIX . 'references.txt'; - } - if (file_exists($referenceFile)) { - $renderer->guideUrl = $guideUrl; - $renderer->guideReferences = []; - foreach (explode("\n", file_get_contents($referenceFile)) as $reference) { - $renderer->guideReferences[BaseRenderer::GUIDE_PREFIX . $reference]['url'] = $renderer->generateGuideUrl($reference); - } - } - - // search for files to process - if (($files = $this->searchFiles($sourceDirs)) === false) { - return 1; - } - - // load context from cache - $context = $this->loadContext($targetDir); - $this->stdout('Checking for updated files... '); - foreach ($context->files as $file => $sha) { - if (!file_exists($file)) { - $this->stdout('At least one file has been removed. Rebuilding the context...'); - $context = new Context(); - if (($files = $this->searchFiles($sourceDirs)) === false) { - return 1; - } - break; - } - if (sha1_file($file) === $sha) { - unset($files[$file]); - } - } - $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); - - // process files - $fileCount = count($files); - $this->stdout($fileCount . ' file' . ($fileCount == 1 ? '' : 's') . ' to update.' . PHP_EOL); - Console::startProgress(0, $fileCount, 'Processing files... ', false); - $done = 0; - foreach ($files as $file) { - $context->addFile($file); - Console::updateProgress(++$done, $fileCount); - } - Console::endProgress(true); - $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); - - // save processed data to cache - $this->storeContext($context, $targetDir); - - $this->updateContext($context); - - // render models - $renderer->controller = $this; - $renderer->render($context, $targetDir); - - if (!empty($context->errors)) { - ArrayHelper::multisort($context->errors, 'file'); - file_put_contents($targetDir . '/errors.txt', print_r($context->errors, true)); - $this->stdout(count($context->errors) . " errors have been logged to $targetDir/errors.txt\n", Console::FG_RED, Console::BOLD); - } - } - - /** - * @inheritdoc - */ - protected function findFiles($path, $except = []) - { - if (empty($except)) { - $except = ['vendor/', 'tests/']; - } - $path = FileHelper::normalizePath($path); - $options = [ - 'filter' => function ($path) { - if (is_file($path)) { - $file = basename($path); - if ($file[0] < 'A' || $file[0] > 'Z') { - return false; - } - } - return null; - }, - 'only' => ['*.php'], - 'except' => $except, - ]; - return FileHelper::findFiles($path, $options); - } - - /** - * @inheritdoc - * @return ApiRenderer - */ - protected function findRenderer($template) - { - $rendererClass = 'yii\\apidoc\\templates\\' . $template . '\\ApiRenderer'; - if (!class_exists($rendererClass)) { - $this->stderr('Renderer not found.' . PHP_EOL); - return false; - } - return new $rendererClass(); - } - - /** - * @inheritdoc - */ - public function options($id) - { - return array_merge(parent::options($id), ['template', 'guide']); - } + /** + * @var string url to where the guide files are located + */ + public $guide; + + // TODO add force update option + + /** + * Renders API documentation files + * @param array $sourceDirs + * @param string $targetDir + * @return int + */ + public function actionIndex(array $sourceDirs, $targetDir) + { + $renderer = $this->findRenderer($this->template); + $targetDir = $this->normalizeTargetDir($targetDir); + if ($targetDir === false || $renderer === false) { + return 1; + } + + $renderer->apiUrl = './'; + + // setup reference to guide + if ($this->guide !== null) { + $guideUrl = $this->guide; + $referenceFile = $guideUrl . '/' . BaseRenderer::GUIDE_PREFIX . 'references.txt'; + } else { + $guideUrl = './'; + $referenceFile = $targetDir . '/' . BaseRenderer::GUIDE_PREFIX . 'references.txt'; + } + if (file_exists($referenceFile)) { + $renderer->guideUrl = $guideUrl; + $renderer->guideReferences = []; + foreach (explode("\n", file_get_contents($referenceFile)) as $reference) { + $renderer->guideReferences[BaseRenderer::GUIDE_PREFIX . $reference]['url'] = $renderer->generateGuideUrl($reference); + } + } + + // search for files to process + if (($files = $this->searchFiles($sourceDirs)) === false) { + return 1; + } + + // load context from cache + $context = $this->loadContext($targetDir); + $this->stdout('Checking for updated files... '); + foreach ($context->files as $file => $sha) { + if (!file_exists($file)) { + $this->stdout('At least one file has been removed. Rebuilding the context...'); + $context = new Context(); + if (($files = $this->searchFiles($sourceDirs)) === false) { + return 1; + } + break; + } + if (sha1_file($file) === $sha) { + unset($files[$file]); + } + } + $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); + + // process files + $fileCount = count($files); + $this->stdout($fileCount . ' file' . ($fileCount == 1 ? '' : 's') . ' to update.' . PHP_EOL); + Console::startProgress(0, $fileCount, 'Processing files... ', false); + $done = 0; + foreach ($files as $file) { + $context->addFile($file); + Console::updateProgress(++$done, $fileCount); + } + Console::endProgress(true); + $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); + + // save processed data to cache + $this->storeContext($context, $targetDir); + + $this->updateContext($context); + + // render models + $renderer->controller = $this; + $renderer->render($context, $targetDir); + + if (!empty($context->errors)) { + ArrayHelper::multisort($context->errors, 'file'); + file_put_contents($targetDir . '/errors.txt', print_r($context->errors, true)); + $this->stdout(count($context->errors) . " errors have been logged to $targetDir/errors.txt\n", Console::FG_RED, Console::BOLD); + } + } + + /** + * @inheritdoc + */ + protected function findFiles($path, $except = []) + { + if (empty($except)) { + $except = ['vendor/', 'tests/']; + } + $path = FileHelper::normalizePath($path); + $options = [ + 'filter' => function ($path) { + if (is_file($path)) { + $file = basename($path); + if ($file[0] < 'A' || $file[0] > 'Z') { + return false; + } + } + + return null; + }, + 'only' => ['*.php'], + 'except' => $except, + ]; + + return FileHelper::findFiles($path, $options); + } + + /** + * @inheritdoc + * @return ApiRenderer + */ + protected function findRenderer($template) + { + $rendererClass = 'yii\\apidoc\\templates\\' . $template . '\\ApiRenderer'; + if (!class_exists($rendererClass)) { + $this->stderr('Renderer not found.' . PHP_EOL); + + return false; + } + + return new $rendererClass(); + } + + /** + * @inheritdoc + */ + public function options($id) + { + return array_merge(parent::options($id), ['template', 'guide']); + } } diff --git a/extensions/apidoc/commands/GuideController.php b/extensions/apidoc/commands/GuideController.php index 83b68a90767..933e76b8855 100644 --- a/extensions/apidoc/commands/GuideController.php +++ b/extensions/apidoc/commands/GuideController.php @@ -23,96 +23,99 @@ */ class GuideController extends BaseController { - /** - * @var string path or URL to the api docs to allow links to classes and properties/methods. - */ - public $apiDocs; - - /** - * Renders API documentation files - * @param array $sourceDirs - * @param string $targetDir - * @return int - */ - public function actionIndex(array $sourceDirs, $targetDir) - { - $renderer = $this->findRenderer($this->template); - $targetDir = $this->normalizeTargetDir($targetDir); - if ($targetDir === false || $renderer === false) { - return 1; - } - - $renderer->guideUrl = './'; - - // setup reference to apidoc - if ($this->apiDocs !== null) { - $renderer->apiUrl = $this->apiDocs; - $renderer->apiContext = $this->loadContext($this->apiDocs); - } elseif (file_exists($targetDir . '/cache/apidoc.data')) { - $renderer->apiUrl = './'; - $renderer->apiContext = $this->loadContext($targetDir); - } else { - $renderer->apiContext = new Context(); - } - $this->updateContext($renderer->apiContext); - - // search for files to process - if (($files = $this->searchFiles($sourceDirs)) === false) { - return 1; - } - - $renderer->controller = $this; - $renderer->render($files, $targetDir); - - $this->stdout('Publishing images...'); - foreach ($sourceDirs as $source) { - FileHelper::copyDirectory(rtrim($source, '/\\') . '/images', $targetDir . '/images'); - } - $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); - - // generate api references.txt - $references = []; - foreach ($files as $file) { - $references[] = basename($file, '.md'); - } - file_put_contents($targetDir . '/guide-references.txt', implode("\n", $references)); - } - - /** - * @inheritdoc - */ - protected function findFiles($path, $except = []) - { - if (empty($except)) { - $except = ['README.md']; - } - $path = FileHelper::normalizePath($path); - $options = [ - 'only' => ['*.md'], - 'except' => $except, - ]; - return FileHelper::findFiles($path, $options); - } - - /** - * @inheritdoc - * @return GuideRenderer - */ - protected function findRenderer($template) - { - $rendererClass = 'yii\\apidoc\\templates\\' . $template . '\\GuideRenderer'; - if (!class_exists($rendererClass)) { - $this->stderr('Renderer not found.' . PHP_EOL); - return false; - } - return new $rendererClass(); - } - - /** - * @inheritdoc - */ - public function options($id) - { - return array_merge(parent::options($id), ['apiDocs']); - } + /** + * @var string path or URL to the api docs to allow links to classes and properties/methods. + */ + public $apiDocs; + + /** + * Renders API documentation files + * @param array $sourceDirs + * @param string $targetDir + * @return int + */ + public function actionIndex(array $sourceDirs, $targetDir) + { + $renderer = $this->findRenderer($this->template); + $targetDir = $this->normalizeTargetDir($targetDir); + if ($targetDir === false || $renderer === false) { + return 1; + } + + $renderer->guideUrl = './'; + + // setup reference to apidoc + if ($this->apiDocs !== null) { + $renderer->apiUrl = $this->apiDocs; + $renderer->apiContext = $this->loadContext($this->apiDocs); + } elseif (file_exists($targetDir . '/cache/apidoc.data')) { + $renderer->apiUrl = './'; + $renderer->apiContext = $this->loadContext($targetDir); + } else { + $renderer->apiContext = new Context(); + } + $this->updateContext($renderer->apiContext); + + // search for files to process + if (($files = $this->searchFiles($sourceDirs)) === false) { + return 1; + } + + $renderer->controller = $this; + $renderer->render($files, $targetDir); + + $this->stdout('Publishing images...'); + foreach ($sourceDirs as $source) { + FileHelper::copyDirectory(rtrim($source, '/\\') . '/images', $targetDir . '/images'); + } + $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); + + // generate api references.txt + $references = []; + foreach ($files as $file) { + $references[] = basename($file, '.md'); + } + file_put_contents($targetDir . '/guide-references.txt', implode("\n", $references)); + } + + /** + * @inheritdoc + */ + protected function findFiles($path, $except = []) + { + if (empty($except)) { + $except = ['README.md']; + } + $path = FileHelper::normalizePath($path); + $options = [ + 'only' => ['*.md'], + 'except' => $except, + ]; + + return FileHelper::findFiles($path, $options); + } + + /** + * @inheritdoc + * @return GuideRenderer + */ + protected function findRenderer($template) + { + $rendererClass = 'yii\\apidoc\\templates\\' . $template . '\\GuideRenderer'; + if (!class_exists($rendererClass)) { + $this->stderr('Renderer not found.' . PHP_EOL); + + return false; + } + + return new $rendererClass(); + } + + /** + * @inheritdoc + */ + public function options($id) + { + return array_merge(parent::options($id), ['apiDocs']); + } } diff --git a/extensions/apidoc/components/BaseController.php b/extensions/apidoc/components/BaseController.php index 82460ee433b..9d700b27dbf 100644 --- a/extensions/apidoc/components/BaseController.php +++ b/extensions/apidoc/components/BaseController.php @@ -21,108 +21,113 @@ */ abstract class BaseController extends Controller { - /** - * @var string template to use for rendering - */ - public $template = 'bootstrap'; - /** - * @var string|array files to exclude. - */ - public $exclude; - - - protected function normalizeTargetDir($target) - { - $target = rtrim(Yii::getAlias($target), '\\/'); - if (file_exists($target)) { - if (is_dir($target) && !$this->confirm('TargetDirectory already exists. Overwrite?', true)) { - $this->stderr('User aborted.' . PHP_EOL); - return false; - } - if (is_file($target)) { - $this->stderr("Error: Target directory \"$target\" is a file!" . PHP_EOL); - return false; - } - } else { - mkdir($target, 0777, true); - } - return $target; - } - - protected function searchFiles($sourceDirs) - { - $this->stdout('Searching files to process... '); - $files = []; - - if (is_array($this->exclude)) { - $exclude = $this->exclude; - } elseif (is_string($this->exclude)) { - $exclude = explode(',', $this->exclude); - } else { - $exclude = []; - } - - foreach ($sourceDirs as $source) { - foreach ($this->findFiles($source, $exclude) as $fileName) { - $files[$fileName] = $fileName; - } - } - $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); - - if (empty($files)) { - $this->stderr('Error: No files found to process.' . PHP_EOL); - return false; - } - return $files; - } - - protected abstract function findFiles($dir, $except = []); - - protected function loadContext($location) - { - $context = new Context(); - - $cacheFile = $location . '/cache/apidoc.data'; - $this->stdout('Loading apidoc data from cache... '); - if (file_exists($cacheFile)) { - $context = unserialize(file_get_contents($cacheFile)); - $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); - } else { - $this->stdout('no data available.' . PHP_EOL, Console::FG_YELLOW); - } - return $context; - } - - protected function storeContext($context, $location) - { - $cacheFile = $location . '/cache/apidoc.data'; - if (!is_dir($dir = dirname($cacheFile))) { - mkdir($dir, 0777, true); - } - file_put_contents($cacheFile, serialize($context)); - } - - /** - * @param Context $context - */ - protected function updateContext($context) - { - $this->stdout('Updating cross references and backlinks... '); - $context->updateReferences(); - $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); - } - - /** - * @param string $template - * @return BaseRenderer - */ - protected abstract function findRenderer($template); - - /** - * @inheritdoc - */ - public function options($id) - { - return array_merge(parent::options($id), ['template', 'exclude']); - } + /** + * @var string template to use for rendering + */ + public $template = 'bootstrap'; + /** + * @var string|array files to exclude. + */ + public $exclude; + + protected function normalizeTargetDir($target) + { + $target = rtrim(Yii::getAlias($target), '\\/'); + if (file_exists($target)) { + if (is_dir($target) && !$this->confirm('TargetDirectory already exists. Overwrite?', true)) { + $this->stderr('User aborted.' . PHP_EOL); + + return false; + } + if (is_file($target)) { + $this->stderr("Error: Target directory \"$target\" is a file!" . PHP_EOL); + + return false; + } + } else { + mkdir($target, 0777, true); + } + + return $target; + } + + protected function searchFiles($sourceDirs) + { + $this->stdout('Searching files to process... '); + $files = []; + + if (is_array($this->exclude)) { + $exclude = $this->exclude; + } elseif (is_string($this->exclude)) { + $exclude = explode(',', $this->exclude); + } else { + $exclude = []; + } + + foreach ($sourceDirs as $source) { + foreach ($this->findFiles($source, $exclude) as $fileName) { + $files[$fileName] = $fileName; + } + } + $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); + + if (empty($files)) { + $this->stderr('Error: No files found to process.' . PHP_EOL); + + return false; + } + + return $files; + } + + abstract protected function findFiles($dir, $except = []); + + protected function loadContext($location) + { + $context = new Context(); + + $cacheFile = $location . '/cache/apidoc.data'; + $this->stdout('Loading apidoc data from cache... '); + if (file_exists($cacheFile)) { + $context = unserialize(file_get_contents($cacheFile)); + $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); + } else { + $this->stdout('no data available.' . PHP_EOL, Console::FG_YELLOW); + } + + return $context; + } + + protected function storeContext($context, $location) + { + $cacheFile = $location . '/cache/apidoc.data'; + if (!is_dir($dir = dirname($cacheFile))) { + mkdir($dir, 0777, true); + } + file_put_contents($cacheFile, serialize($context)); + } + + /** + * @param Context $context + */ + protected function updateContext($context) + { + $this->stdout('Updating cross references and backlinks... '); + $context->updateReferences(); + $this->stdout('done.' . PHP_EOL, Console::FG_GREEN); + } + + /** + * @param string $template + * @return BaseRenderer + */ + abstract protected function findRenderer($template); + + /** + * @inheritdoc + */ + public function options($id) + { + return array_merge(parent::options($id), ['template', 'exclude']); + } } diff --git a/extensions/apidoc/helpers/ApiMarkdown.php b/extensions/apidoc/helpers/ApiMarkdown.php index 690323d8aab..d127b1af469 100644 --- a/extensions/apidoc/helpers/ApiMarkdown.php +++ b/extensions/apidoc/helpers/ApiMarkdown.php @@ -23,225 +23,234 @@ */ class ApiMarkdown extends GithubMarkdown { - /** - * @var BaseRenderer - */ - public static $renderer; - - protected $context; - - public function prepare() - { - parent::prepare(); - - // add references to guide pages - $this->references = array_merge($this->references, static::$renderer->guideReferences); - } - - /** - * @inheritDoc - */ - protected function identifyLine($lines, $current) - { - if (strncmp($lines[$current], '~~~', 3) === 0) { - return 'fencedCode'; - } - return parent::identifyLine($lines, $current); - } - - /** - * Consume lines for a fenced code block - */ - protected function consumeFencedCode($lines, $current) - { - // consume until ``` - $block = [ - 'type' => 'code', - 'content' => [], - ]; - $line = rtrim($lines[$current]); - if (strncmp($lines[$current], '~~~', 3) === 0) { - $fence = '~~~'; - $language = 'php'; - } else { - $fence = substr($line, 0, $pos = strrpos($line, '`') + 1); - $language = substr($line, $pos); - } - if (!empty($language)) { - $block['language'] = $language; - } - for ($i = $current + 1, $count = count($lines); $i < $count; $i++) { - if (rtrim($line = $lines[$i]) !== $fence) { - $block['content'][] = $line; - } else { - break; - } - } - return [$block, $i]; - } - - /** - * Renders a code block - */ - protected function renderCode($block) - { - if (isset($block['language'])) { - $class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : ''; - return "
" . $this->highlight(implode("\n", $block['content']) . "\n", $block['language']) . '
'; - } else { - return parent::renderCode($block); - } - } - - public static function highlight($code, $language) - { - if ($language !== 'php') { - return htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8'); - } - - // TODO improve code highlighting - if (strncmp($code, '\n and tags added by php - $text = substr(trim($text), 36, -16); - - return $text; - } - - protected function inlineMarkers() - { - return array_merge(parent::inlineMarkers(), [ - '[[' => 'parseApiLinks', - ]); - } - - protected function parseApiLinks($text) - { - $context = $this->context; - - if (preg_match('/^\[\[([\w\d\\\\\(\):$]+)(\|[^\]]*)?\]\]/', $text, $matches)) { - - $offset = strlen($matches[0]); - - $object = $matches[1]; - $title = (empty($matches[2]) || $matches[2] == '|') ? null : substr($matches[2], 1); - - if (($pos = strpos($object, '::')) !== false) { - $typeName = substr($object, 0, $pos); - $subjectName = substr($object, $pos + 2); - if ($context !== null) { - // Collection resolves relative types - $typeName = (new Collection([$typeName], $context->phpDocContext))->__toString(); - } - $type = static::$renderer->apiContext->getType($typeName); - if ($type === null) { - static::$renderer->apiContext->errors[] = [ - 'file' => ($context !== null) ? $context->sourceFile : null, - 'message' => 'broken link to ' . $typeName . '::' . $subjectName . (($context !== null) ? ' in ' . $context->name : ''), - ]; - return [ - '' . $typeName . '::' . $subjectName . '', - $offset - ]; - } else { - if (($subject = $type->findSubject($subjectName)) !== null) { - if ($title === null) { - $title = $type->name . '::' . $subject->name; - if ($subject instanceof MethodDoc) { - $title .= '()'; - } - } - return [ - static::$renderer->createSubjectLink($subject, $title), - $offset - ]; - } else { - static::$renderer->apiContext->errors[] = [ - 'file' => ($context !== null) ? $context->sourceFile : null, - 'message' => 'broken link to ' . $type->name . '::' . $subjectName . (($context !== null) ? ' in ' . $context->name : ''), - ]; - return [ - '' . $type->name . '::' . $subjectName . '', - $offset - ]; - } - } - } elseif ($context !== null && ($subject = $context->findSubject($object)) !== null) { - return [ - static::$renderer->createSubjectLink($subject, $title), - $offset - ]; - } - - if ($context !== null) { - // Collection resolves relative types - $object = (new Collection([$object], $context->phpDocContext))->__toString(); - } - if (($type = static::$renderer->apiContext->getType($object)) !== null) { - return [ - static::$renderer->createTypeLink($type, null, $title), - $offset - ]; - } elseif (strpos($typeLink = static::$renderer->createTypeLink($object, null, $title), 'apiContext->errors[] = [ - 'file' => ($context !== null) ? $context->sourceFile : null, - 'message' => 'broken link to ' . $object . (($context !== null) ? ' in ' . $context->name : ''), - ]; - return [ - '' . $object . '', - $offset - ]; - } - return ['[[', 2]; - } - - /** - * @inheritDoc - */ - protected function renderHeadline($block) - { - $content = $this->parseInline($block['content']); - $hash = Inflector::slug(strip_tags($content)); - $hashLink = ""; - $tag = 'h' . $block['level']; - return "<$tag>$content $hashLink"; - } - - /** - * Converts markdown into HTML - * - * @param string $content - * @param TypeDoc $context - * @param boolean $paragraph - * @return string - */ - public static function process($content, $context = null, $paragraph = false) - { - if (!isset(Markdown::$flavors['api'])) { - Markdown::$flavors['api'] = new static; - } - - if (is_string($context)) { - $context = static::$renderer->apiContext->getType($context); - } - Markdown::$flavors['api']->context = $context; - - if ($paragraph) { - return Markdown::processParagraph($content, 'api'); - } else { - return Markdown::process($content, 'api'); - } - } + /** + * @var BaseRenderer + */ + public static $renderer; + + protected $context; + + public function prepare() + { + parent::prepare(); + + // add references to guide pages + $this->references = array_merge($this->references, static::$renderer->guideReferences); + } + + /** + * @inheritDoc + */ + protected function identifyLine($lines, $current) + { + if (strncmp($lines[$current], '~~~', 3) === 0) { + return 'fencedCode'; + } + + return parent::identifyLine($lines, $current); + } + + /** + * Consume lines for a fenced code block + */ + protected function consumeFencedCode($lines, $current) + { + // consume until ``` + $block = [ + 'type' => 'code', + 'content' => [], + ]; + $line = rtrim($lines[$current]); + if (strncmp($lines[$current], '~~~', 3) === 0) { + $fence = '~~~'; + $language = 'php'; + } else { + $fence = substr($line, 0, $pos = strrpos($line, '`') + 1); + $language = substr($line, $pos); + } + if (!empty($language)) { + $block['language'] = $language; + } + for ($i = $current + 1, $count = count($lines); $i < $count; $i++) { + if (rtrim($line = $lines[$i]) !== $fence) { + $block['content'][] = $line; + } else { + break; + } + } + + return [$block, $i]; + } + + /** + * Renders a code block + */ + protected function renderCode($block) + { + if (isset($block['language'])) { + $class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : ''; + + return "
" . $this->highlight(implode("\n", $block['content']) . "\n", $block['language']) . '
'; + } else { + return parent::renderCode($block); + } + } + + public static function highlight($code, $language) + { + if ($language !== 'php') { + return htmlspecialchars($code, ENT_NOQUOTES, 'UTF-8'); + } + + // TODO improve code highlighting + if (strncmp($code, '\n and tags added by php + $text = substr(trim($text), 36, -16); + + return $text; + } + + protected function inlineMarkers() + { + return array_merge(parent::inlineMarkers(), [ + '[[' => 'parseApiLinks', + ]); + } + + protected function parseApiLinks($text) + { + $context = $this->context; + + if (preg_match('/^\[\[([\w\d\\\\\(\):$]+)(\|[^\]]*)?\]\]/', $text, $matches)) { + + $offset = strlen($matches[0]); + + $object = $matches[1]; + $title = (empty($matches[2]) || $matches[2] == '|') ? null : substr($matches[2], 1); + + if (($pos = strpos($object, '::')) !== false) { + $typeName = substr($object, 0, $pos); + $subjectName = substr($object, $pos + 2); + if ($context !== null) { + // Collection resolves relative types + $typeName = (new Collection([$typeName], $context->phpDocContext))->__toString(); + } + $type = static::$renderer->apiContext->getType($typeName); + if ($type === null) { + static::$renderer->apiContext->errors[] = [ + 'file' => ($context !== null) ? $context->sourceFile : null, + 'message' => 'broken link to ' . $typeName . '::' . $subjectName . (($context !== null) ? ' in ' . $context->name : ''), + ]; + + return [ + '' . $typeName . '::' . $subjectName . '', + $offset + ]; + } else { + if (($subject = $type->findSubject($subjectName)) !== null) { + if ($title === null) { + $title = $type->name . '::' . $subject->name; + if ($subject instanceof MethodDoc) { + $title .= '()'; + } + } + + return [ + static::$renderer->createSubjectLink($subject, $title), + $offset + ]; + } else { + static::$renderer->apiContext->errors[] = [ + 'file' => ($context !== null) ? $context->sourceFile : null, + 'message' => 'broken link to ' . $type->name . '::' . $subjectName . (($context !== null) ? ' in ' . $context->name : ''), + ]; + + return [ + '' . $type->name . '::' . $subjectName . '', + $offset + ]; + } + } + } elseif ($context !== null && ($subject = $context->findSubject($object)) !== null) { + return [ + static::$renderer->createSubjectLink($subject, $title), + $offset + ]; + } + + if ($context !== null) { + // Collection resolves relative types + $object = (new Collection([$object], $context->phpDocContext))->__toString(); + } + if (($type = static::$renderer->apiContext->getType($object)) !== null) { + return [ + static::$renderer->createTypeLink($type, null, $title), + $offset + ]; + } elseif (strpos($typeLink = static::$renderer->createTypeLink($object, null, $title), 'apiContext->errors[] = [ + 'file' => ($context !== null) ? $context->sourceFile : null, + 'message' => 'broken link to ' . $object . (($context !== null) ? ' in ' . $context->name : ''), + ]; + + return [ + '' . $object . '', + $offset + ]; + } + + return ['[[', 2]; + } + + /** + * @inheritDoc + */ + protected function renderHeadline($block) + { + $content = $this->parseInline($block['content']); + $hash = Inflector::slug(strip_tags($content)); + $hashLink = ""; + $tag = 'h' . $block['level']; + + return "<$tag>$content $hashLink"; + } + + /** + * Converts markdown into HTML + * + * @param string $content + * @param TypeDoc $context + * @param boolean $paragraph + * @return string + */ + public static function process($content, $context = null, $paragraph = false) + { + if (!isset(Markdown::$flavors['api'])) { + Markdown::$flavors['api'] = new static; + } + + if (is_string($context)) { + $context = static::$renderer->apiContext->getType($context); + } + Markdown::$flavors['api']->context = $context; + + if ($paragraph) { + return Markdown::processParagraph($content, 'api'); + } else { + return Markdown::process($content, 'api'); + } + } } diff --git a/extensions/apidoc/helpers/PrettyPrinter.php b/extensions/apidoc/helpers/PrettyPrinter.php index 9085cf04d1c..3370e865546 100644 --- a/extensions/apidoc/helpers/PrettyPrinter.php +++ b/extensions/apidoc/helpers/PrettyPrinter.php @@ -18,24 +18,25 @@ */ class PrettyPrinter extends \phpDocumentor\Reflection\PrettyPrinter { - public function pExpr_Array(PHPParser_Node_Expr_Array $node) - { - return '[' . $this->pCommaSeparated($node->items) . ']'; - } + public function pExpr_Array(PHPParser_Node_Expr_Array $node) + { + return '[' . $this->pCommaSeparated($node->items) . ']'; + } - /** - * Returns a simple human readable output for a value. - * - * @param PHPParser_Node_Expr $value The value node as provided by PHP-Parser. - * @return string - */ - public static function getRepresentationOfValue(PHPParser_Node_Expr $value) - { - if ($value === null) { - return ''; - } + /** + * Returns a simple human readable output for a value. + * + * @param PHPParser_Node_Expr $value The value node as provided by PHP-Parser. + * @return string + */ + public static function getRepresentationOfValue(PHPParser_Node_Expr $value) + { + if ($value === null) { + return ''; + } - $printer = new static(); - return $printer->prettyPrintExpr($value); - } + $printer = new static(); + + return $printer->prettyPrintExpr($value); + } } diff --git a/extensions/apidoc/models/BaseDoc.php b/extensions/apidoc/models/BaseDoc.php index 054b0847a89..c95876abe6f 100644 --- a/extensions/apidoc/models/BaseDoc.php +++ b/extensions/apidoc/models/BaseDoc.php @@ -20,82 +20,81 @@ */ class BaseDoc extends Object { - /** - * @var \phpDocumentor\Reflection\DocBlock\Context - */ - public $phpDocContext; + /** + * @var \phpDocumentor\Reflection\DocBlock\Context + */ + public $phpDocContext; - public $name; + public $name; - public $sourceFile; - public $startLine; - public $endLine; + public $sourceFile; + public $startLine; + public $endLine; - public $shortDescription; - public $description; - public $since; - public $deprecatedSince; - public $deprecatedReason; + public $shortDescription; + public $description; + public $since; + public $deprecatedSince; + public $deprecatedReason; - /** - * @var \phpDocumentor\Reflection\DocBlock\Tag[] - */ - public $tags = []; + /** + * @var \phpDocumentor\Reflection\DocBlock\Tag[] + */ + public $tags = []; + /** + * @param \phpDocumentor\Reflection\BaseReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($config); - /** - * @param \phpDocumentor\Reflection\BaseReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($config); + if ($reflector === null) { + return; + } - if ($reflector === null) { - return; - } + // base properties + $this->name = ltrim($reflector->getName(), '\\'); + $this->startLine = $reflector->getNode()->getAttribute('startLine'); + $this->endLine = $reflector->getNode()->getAttribute('endLine'); - // base properties - $this->name = ltrim($reflector->getName(), '\\'); - $this->startLine = $reflector->getNode()->getAttribute('startLine'); - $this->endLine = $reflector->getNode()->getAttribute('endLine'); + $docblock = $reflector->getDocBlock(); + if ($docblock !== null) { + $this->shortDescription = ucfirst($docblock->getShortDescription()); + if (empty($this->shortDescription) && !($this instanceof PropertyDoc) && $context !== null) { + $context->errors[] = [ + 'line' => $this->startLine, + 'file' => $this->sourceFile, + 'message' => "No short description for " . substr(StringHelper::basename(get_class($this)), 0, -3) . " '{$this->name}'", + ]; + } + $this->description = $docblock->getLongDescription(); - $docblock = $reflector->getDocBlock(); - if ($docblock !== null) { - $this->shortDescription = ucfirst($docblock->getShortDescription()); - if (empty($this->shortDescription) && !($this instanceof PropertyDoc) && $context !== null) { - $context->errors[] = [ - 'line' => $this->startLine, - 'file' => $this->sourceFile, - 'message' => "No short description for " . substr(StringHelper::basename(get_class($this)), 0, -3) . " '{$this->name}'", - ]; - } - $this->description = $docblock->getLongDescription(); + $this->phpDocContext = $docblock->getContext(); - $this->phpDocContext = $docblock->getContext(); + $this->tags = $docblock->getTags(); + foreach ($this->tags as $i => $tag) { + if ($tag instanceof SinceTag) { + $this->since = $tag->getVersion(); + unset($this->tags[$i]); + } elseif ($tag instanceof DeprecatedTag) { + $this->deprecatedSince = $tag->getVersion(); + $this->deprecatedReason = $tag->getDescription(); + unset($this->tags[$i]); + } + } + } elseif ($context !== null) { + $context->errors[] = [ + 'line' => $this->startLine, + 'file' => $this->sourceFile, + 'message' => "No docblock for element '{$this->name}'", + ]; + } + } - $this->tags = $docblock->getTags(); - foreach ($this->tags as $i => $tag) { - if ($tag instanceof SinceTag) { - $this->since = $tag->getVersion(); - unset($this->tags[$i]); - } elseif ($tag instanceof DeprecatedTag) { - $this->deprecatedSince = $tag->getVersion(); - $this->deprecatedReason = $tag->getDescription(); - unset($this->tags[$i]); - } - } - } elseif ($context !== null) { - $context->errors[] = [ - 'line' => $this->startLine, - 'file' => $this->sourceFile, - 'message' => "No docblock for element '{$this->name}'", - ]; - } - } - - // TODO implement + // TODO implement // public function loadSource($reflection) // { // $this->sourceFile; diff --git a/extensions/apidoc/models/ClassDoc.php b/extensions/apidoc/models/ClassDoc.php index 53cd58fe263..1bb4d70c003 100644 --- a/extensions/apidoc/models/ClassDoc.php +++ b/extensions/apidoc/models/ClassDoc.php @@ -17,99 +17,100 @@ */ class ClassDoc extends TypeDoc { - public $parentClass; + public $parentClass; - public $isAbstract; - public $isFinal; + public $isAbstract; + public $isFinal; - /** - * @var string[] - */ - public $interfaces = []; - public $traits = []; - // will be set by Context::updateReferences() - public $subclasses = []; + /** + * @var string[] + */ + public $interfaces = []; + public $traits = []; + // will be set by Context::updateReferences() + public $subclasses = []; - /** - * @var EventDoc[] - */ - public $events = []; - /** - * @var ConstDoc[] - */ - public $constants = []; + /** + * @var EventDoc[] + */ + public $events = []; + /** + * @var ConstDoc[] + */ + public $constants = []; + public function findSubject($subjectName) + { + if (($subject = parent::findSubject($subjectName)) !== null) { + return $subject; + } + foreach ($this->events as $name => $event) { + if ($subjectName == $name) { + return $event; + } + } + foreach ($this->constants as $name => $constant) { + if ($subjectName == $name) { + return $constant; + } + } - public function findSubject($subjectName) - { - if (($subject = parent::findSubject($subjectName)) !== null) { - return $subject; - } - foreach ($this->events as $name => $event) { - if ($subjectName == $name) { - return $event; - } - } - foreach ($this->constants as $name => $constant) { - if ($subjectName == $name) { - return $constant; - } - } - return null; - } + return null; + } - /** - * @return EventDoc[] - */ - public function getNativeEvents() - { - $events = []; - foreach ($this->events as $name => $event) { - if ($event->definedBy != $this->name) { - continue; - } - $events[$name] = $event; - } - return $events; - } + /** + * @return EventDoc[] + */ + public function getNativeEvents() + { + $events = []; + foreach ($this->events as $name => $event) { + if ($event->definedBy != $this->name) { + continue; + } + $events[$name] = $event; + } - /** - * @param \phpDocumentor\Reflection\ClassReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + return $events; + } - if ($reflector === null) { - return; - } + /** + * @param \phpDocumentor\Reflection\ClassReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - $this->parentClass = ltrim($reflector->getParentClass(), '\\'); - if (empty($this->parentClass)) { - $this->parentClass = null; - } - $this->isAbstract = $reflector->isAbstract(); - $this->isFinal = $reflector->isFinal(); + if ($reflector === null) { + return; + } - foreach ($reflector->getInterfaces() as $interface) { - $this->interfaces[] = ltrim($interface, '\\'); - } - foreach ($reflector->getTraits() as $trait) { - $this->traits[] = ltrim($trait, '\\'); - } - foreach ($reflector->getConstants() as $constantReflector) { - $docblock = $constantReflector->getDocBlock(); - if ($docblock !== null && count($docblock->getTagsByName('event')) > 0) { - $event = new EventDoc($constantReflector); - $event->definedBy = $this->name; - $this->events[$event->name] = $event; - } else { - $constant = new ConstDoc($constantReflector); - $constant->definedBy = $this->name; - $this->constants[$constant->name] = $constant; - } - } - } + $this->parentClass = ltrim($reflector->getParentClass(), '\\'); + if (empty($this->parentClass)) { + $this->parentClass = null; + } + $this->isAbstract = $reflector->isAbstract(); + $this->isFinal = $reflector->isFinal(); + + foreach ($reflector->getInterfaces() as $interface) { + $this->interfaces[] = ltrim($interface, '\\'); + } + foreach ($reflector->getTraits() as $trait) { + $this->traits[] = ltrim($trait, '\\'); + } + foreach ($reflector->getConstants() as $constantReflector) { + $docblock = $constantReflector->getDocBlock(); + if ($docblock !== null && count($docblock->getTagsByName('event')) > 0) { + $event = new EventDoc($constantReflector); + $event->definedBy = $this->name; + $this->events[$event->name] = $event; + } else { + $constant = new ConstDoc($constantReflector); + $constant->definedBy = $this->name; + $this->constants[$constant->name] = $constant; + } + } + } } diff --git a/extensions/apidoc/models/ConstDoc.php b/extensions/apidoc/models/ConstDoc.php index abca13f5605..69fd6027992 100644 --- a/extensions/apidoc/models/ConstDoc.php +++ b/extensions/apidoc/models/ConstDoc.php @@ -15,22 +15,22 @@ */ class ConstDoc extends BaseDoc { - public $definedBy; - public $value; + public $definedBy; + public $value; - /** - * @param \phpDocumentor\Reflection\ClassReflector\ConstantReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + /** + * @param \phpDocumentor\Reflection\ClassReflector\ConstantReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - if ($reflector === null) { - return; - } + if ($reflector === null) { + return; + } - $this->value = $reflector->getValue(); - } + $this->value = $reflector->getValue(); + } } diff --git a/extensions/apidoc/models/Context.php b/extensions/apidoc/models/Context.php index d509d1611e6..bf3700f049d 100644 --- a/extensions/apidoc/models/Context.php +++ b/extensions/apidoc/models/Context.php @@ -17,305 +17,310 @@ */ class Context extends Component { - /** - * @var array list of php files that have been added to this context. - */ - public $files = []; - /** - * @var ClassDoc[] - */ - public $classes = []; - /** - * @var InterfaceDoc[] - */ - public $interfaces = []; - /** - * @var TraitDoc[] - */ - public $traits = []; + /** + * @var array list of php files that have been added to this context. + */ + public $files = []; + /** + * @var ClassDoc[] + */ + public $classes = []; + /** + * @var InterfaceDoc[] + */ + public $interfaces = []; + /** + * @var TraitDoc[] + */ + public $traits = []; - public $errors = []; + public $errors = []; + public function getType($type) + { + $type = ltrim($type, '\\'); + if (isset($this->classes[$type])) { + return $this->classes[$type]; + } elseif (isset($this->interfaces[$type])) { + return $this->interfaces[$type]; + } elseif (isset($this->traits[$type])) { + return $this->traits[$type]; + } - public function getType($type) - { - $type = ltrim($type, '\\'); - if (isset($this->classes[$type])) { - return $this->classes[$type]; - } elseif (isset($this->interfaces[$type])) { - return $this->interfaces[$type]; - } elseif (isset($this->traits[$type])) { - return $this->traits[$type]; - } - return null; - } + return null; + } - public function addFile($fileName) - { - $this->files[$fileName] = sha1_file($fileName); + public function addFile($fileName) + { + $this->files[$fileName] = sha1_file($fileName); - $reflection = new FileReflector($fileName, true); - $reflection->process(); + $reflection = new FileReflector($fileName, true); + $reflection->process(); - foreach ($reflection->getClasses() as $class) { - $class = new ClassDoc($class, $this, ['sourceFile' => $fileName]); - $this->classes[$class->name] = $class; - } - foreach ($reflection->getInterfaces() as $interface) { - $interface = new InterfaceDoc($interface, $this, ['sourceFile' => $fileName]); - $this->interfaces[$interface->name] = $interface; - } - foreach ($reflection->getTraits() as $trait) { - $trait = new TraitDoc($trait, $this, ['sourceFile' => $fileName]); - $this->traits[$trait->name] = $trait; - } - } + foreach ($reflection->getClasses() as $class) { + $class = new ClassDoc($class, $this, ['sourceFile' => $fileName]); + $this->classes[$class->name] = $class; + } + foreach ($reflection->getInterfaces() as $interface) { + $interface = new InterfaceDoc($interface, $this, ['sourceFile' => $fileName]); + $this->interfaces[$interface->name] = $interface; + } + foreach ($reflection->getTraits() as $trait) { + $trait = new TraitDoc($trait, $this, ['sourceFile' => $fileName]); + $this->traits[$trait->name] = $trait; + } + } - public function updateReferences() - { - // update all subclass references - foreach ($this->classes as $class) { - $className = $class->name; - while (isset($this->classes[$class->parentClass])) { - $class = $this->classes[$class->parentClass]; - $class->subclasses[] = $className; - } - } - // update interfaces of subclasses - foreach ($this->classes as $class) { - $this->updateSubclassInferfacesTraits($class); - } - // update implementedBy and usedBy for interfaces and traits - foreach ($this->classes as $class) { - foreach ($class->traits as $trait) { - if (isset($this->traits[$trait])) { - $trait = $this->traits[$trait]; - $trait->usedBy[] = $class->name; - $class->properties = array_merge($trait->properties, $class->properties); - $class->methods = array_merge($trait->methods, $class->methods); - } - } - foreach ($class->interfaces as $interface) { - if (isset($this->interfaces[$interface])) { - $this->interfaces[$interface]->implementedBy[] = $class->name; - if ($class->isAbstract) { - // add not implemented interface methods - foreach ($this->interfaces[$interface]->methods as $method) { - if (!isset($class->methods[$method->name])) { - $class->methods[$method->name] = $method; - } - } - } - } - } - } - // inherit docs - foreach ($this->classes as $class) { - $this->inheritDocs($class); - } - // inherit properties, methods, contants and events to subclasses - foreach ($this->classes as $class) { - $this->updateSubclassInheritance($class); - } - // add properties from getters and setters - foreach ($this->classes as $class) { - $this->handlePropertyFeature($class); - } + public function updateReferences() + { + // update all subclass references + foreach ($this->classes as $class) { + $className = $class->name; + while (isset($this->classes[$class->parentClass])) { + $class = $this->classes[$class->parentClass]; + $class->subclasses[] = $className; + } + } + // update interfaces of subclasses + foreach ($this->classes as $class) { + $this->updateSubclassInferfacesTraits($class); + } + // update implementedBy and usedBy for interfaces and traits + foreach ($this->classes as $class) { + foreach ($class->traits as $trait) { + if (isset($this->traits[$trait])) { + $trait = $this->traits[$trait]; + $trait->usedBy[] = $class->name; + $class->properties = array_merge($trait->properties, $class->properties); + $class->methods = array_merge($trait->methods, $class->methods); + } + } + foreach ($class->interfaces as $interface) { + if (isset($this->interfaces[$interface])) { + $this->interfaces[$interface]->implementedBy[] = $class->name; + if ($class->isAbstract) { + // add not implemented interface methods + foreach ($this->interfaces[$interface]->methods as $method) { + if (!isset($class->methods[$method->name])) { + $class->methods[$method->name] = $method; + } + } + } + } + } + } + // inherit docs + foreach ($this->classes as $class) { + $this->inheritDocs($class); + } + // inherit properties, methods, contants and events to subclasses + foreach ($this->classes as $class) { + $this->updateSubclassInheritance($class); + } + // add properties from getters and setters + foreach ($this->classes as $class) { + $this->handlePropertyFeature($class); + } - // TODO reference exceptions to methods where they are thrown - } + // TODO reference exceptions to methods where they are thrown + } - /** - * Add implemented interfaces and used traits to subclasses - * @param ClassDoc $class - */ - protected function updateSubclassInferfacesTraits($class) - { - foreach ($class->subclasses as $subclass) { - $subclass = $this->classes[$subclass]; - $subclass->interfaces = array_unique(array_merge($subclass->interfaces, $class->interfaces)); - $subclass->traits = array_unique(array_merge($subclass->traits, $class->traits)); - $this->updateSubclassInferfacesTraits($subclass); - } - } + /** + * Add implemented interfaces and used traits to subclasses + * @param ClassDoc $class + */ + protected function updateSubclassInferfacesTraits($class) + { + foreach ($class->subclasses as $subclass) { + $subclass = $this->classes[$subclass]; + $subclass->interfaces = array_unique(array_merge($subclass->interfaces, $class->interfaces)); + $subclass->traits = array_unique(array_merge($subclass->traits, $class->traits)); + $this->updateSubclassInferfacesTraits($subclass); + } + } - /** - * Add implemented interfaces and used traits to subclasses - * @param ClassDoc $class - */ - protected function updateSubclassInheritance($class) - { - foreach ($class->subclasses as $subclass) { - $subclass = $this->classes[$subclass]; - $subclass->events = array_merge($class->events, $subclass->events); - $subclass->constants = array_merge($class->constants, $subclass->constants); - $subclass->properties = array_merge($class->properties, $subclass->properties); - $subclass->methods = array_merge($class->methods, $subclass->methods); - $this->updateSubclassInheritance($subclass); - } - } + /** + * Add implemented interfaces and used traits to subclasses + * @param ClassDoc $class + */ + protected function updateSubclassInheritance($class) + { + foreach ($class->subclasses as $subclass) { + $subclass = $this->classes[$subclass]; + $subclass->events = array_merge($class->events, $subclass->events); + $subclass->constants = array_merge($class->constants, $subclass->constants); + $subclass->properties = array_merge($class->properties, $subclass->properties); + $subclass->methods = array_merge($class->methods, $subclass->methods); + $this->updateSubclassInheritance($subclass); + } + } - /** - * Inhertit docsblocks using `@inheritDoc` tag. - * @param ClassDoc $class - * @see http://phpdoc.org/docs/latest/guides/inheritance.html - */ - protected function inheritDocs($class) - { - // TODO also for properties? - foreach ($class->methods as $m) { - $inheritedMethod = $this->inheritMethodRecursive($m, $class); - foreach (['shortDescription', 'description', 'params', 'return', 'returnType', 'returnTypes', 'exceptions'] as $property) { - if (empty($m->$property)) { - $m->$property = $inheritedMethod->$property; - } - } - } - } + /** + * Inhertit docsblocks using `@inheritDoc` tag. + * @param ClassDoc $class + * @see http://phpdoc.org/docs/latest/guides/inheritance.html + */ + protected function inheritDocs($class) + { + // TODO also for properties? + foreach ($class->methods as $m) { + $inheritedMethod = $this->inheritMethodRecursive($m, $class); + foreach (['shortDescription', 'description', 'params', 'return', 'returnType', 'returnTypes', 'exceptions'] as $property) { + if (empty($m->$property)) { + $m->$property = $inheritedMethod->$property; + } + } + } + } - /** - * @param MethodDoc $method - * @param ClassDoc $parent - */ - private function inheritMethodRecursive($method, $class) - { - if (!isset($this->classes[$class->parentClass])) { - return $method; - } - $parent = $this->classes[$class->parentClass]; - foreach ($method->tags as $tag) { - if (strtolower($tag->getName()) == 'inheritdoc') { - if (isset($parent->methods[$method->name])) { - $method = $parent->methods[$method->name]; - } - return $this->inheritMethodRecursive($method, $parent); - } - } - return $method; - } + /** + * @param MethodDoc $method + * @param ClassDoc $parent + */ + private function inheritMethodRecursive($method, $class) + { + if (!isset($this->classes[$class->parentClass])) { + return $method; + } + $parent = $this->classes[$class->parentClass]; + foreach ($method->tags as $tag) { + if (strtolower($tag->getName()) == 'inheritdoc') { + if (isset($parent->methods[$method->name])) { + $method = $parent->methods[$method->name]; + } - /** - * Add properties for getters and setters if class is subclass of [[\yii\base\Object]]. - * @param ClassDoc $class - */ - protected function handlePropertyFeature($class) - { - if (!$this->isSubclassOf($class, 'yii\base\Object')) { - return; - } - foreach ($class->getPublicMethods() as $name => $method) { - if ($method->isStatic) { - continue; - } - if (!strncmp($name, 'get', 3) && $this->paramsOptional($method)) { - $propertyName = '$' . lcfirst(substr($method->name, 3)); - if (isset($class->properties[$propertyName])) { - $property = $class->properties[$propertyName]; - if ($property->getter === null && $property->setter === null) { - $this->errors[] = [ - 'line' => $property->startLine, - 'file' => $class->sourceFile, - 'message' => "Property $propertyName conflicts with a defined getter {$method->name} in {$class->name}.", - ]; - } - $property->getter = $method; - } else { - $class->properties[$propertyName] = new PropertyDoc(null, $this, [ - 'name' => $propertyName, - 'definedBy' => $class->name, - 'sourceFile' => $class->sourceFile, - 'visibility' => 'public', - 'isStatic' => false, - 'type' => $method->returnType, - 'types' => $method->returnTypes, - 'shortDescription' => (($pos = strpos($method->return, '.')) !== false) ? - substr($method->return, 0, $pos) : $method->return, - 'description' => $method->return, - 'getter' => $method - // TODO set default value - ]); - } - } - if (!strncmp($name, 'set', 3) && $this->paramsOptional($method, 1)) { - $propertyName = '$' . lcfirst(substr($method->name, 3)); - if (isset($class->properties[$propertyName])) { - $property = $class->properties[$propertyName]; - if ($property->getter === null && $property->setter === null) { - $this->errors[] = [ - 'line' => $property->startLine, - 'file' => $class->sourceFile, - 'message' => "Property $propertyName conflicts with a defined setter {$method->name} in {$class->name}.", - ]; - } - $property->setter = $method; - } else { - $param = $this->getFirstNotOptionalParameter($method); - $class->properties[$propertyName] = new PropertyDoc(null, $this, [ - 'name' => $propertyName, - 'definedBy' => $class->name, - 'sourceFile' => $class->sourceFile, - 'visibility' => 'public', - 'isStatic' => false, - 'type' => $param->type, - 'types' => $param->types, - 'shortDescription' => (($pos = strpos($param->description, '.')) !== false) ? - substr($param->description, 0, $pos) : $param->description, - 'description' => $param->description, - 'setter' => $method - ]); - } - } - } - } + return $this->inheritMethodRecursive($method, $parent); + } + } - /** - * @param MethodDoc $method - * @param integer $number number of not optional parameters - * @return bool - */ - private function paramsOptional($method, $number = 0) - { - foreach ($method->params as $param) { - if (!$param->isOptional && $number-- <= 0) { - return false; - } - } - return true; - } + return $method; + } - /** - * @param MethodDoc $method - * @return ParamDoc - */ - private function getFirstNotOptionalParameter($method) - { - foreach ($method->params as $param) { - if (!$param->isOptional) { - return $param; - } - } - return null; - } + /** + * Add properties for getters and setters if class is subclass of [[\yii\base\Object]]. + * @param ClassDoc $class + */ + protected function handlePropertyFeature($class) + { + if (!$this->isSubclassOf($class, 'yii\base\Object')) { + return; + } + foreach ($class->getPublicMethods() as $name => $method) { + if ($method->isStatic) { + continue; + } + if (!strncmp($name, 'get', 3) && $this->paramsOptional($method)) { + $propertyName = '$' . lcfirst(substr($method->name, 3)); + if (isset($class->properties[$propertyName])) { + $property = $class->properties[$propertyName]; + if ($property->getter === null && $property->setter === null) { + $this->errors[] = [ + 'line' => $property->startLine, + 'file' => $class->sourceFile, + 'message' => "Property $propertyName conflicts with a defined getter {$method->name} in {$class->name}.", + ]; + } + $property->getter = $method; + } else { + $class->properties[$propertyName] = new PropertyDoc(null, $this, [ + 'name' => $propertyName, + 'definedBy' => $class->name, + 'sourceFile' => $class->sourceFile, + 'visibility' => 'public', + 'isStatic' => false, + 'type' => $method->returnType, + 'types' => $method->returnTypes, + 'shortDescription' => (($pos = strpos($method->return, '.')) !== false) ? + substr($method->return, 0, $pos) : $method->return, + 'description' => $method->return, + 'getter' => $method + // TODO set default value + ]); + } + } + if (!strncmp($name, 'set', 3) && $this->paramsOptional($method, 1)) { + $propertyName = '$' . lcfirst(substr($method->name, 3)); + if (isset($class->properties[$propertyName])) { + $property = $class->properties[$propertyName]; + if ($property->getter === null && $property->setter === null) { + $this->errors[] = [ + 'line' => $property->startLine, + 'file' => $class->sourceFile, + 'message' => "Property $propertyName conflicts with a defined setter {$method->name} in {$class->name}.", + ]; + } + $property->setter = $method; + } else { + $param = $this->getFirstNotOptionalParameter($method); + $class->properties[$propertyName] = new PropertyDoc(null, $this, [ + 'name' => $propertyName, + 'definedBy' => $class->name, + 'sourceFile' => $class->sourceFile, + 'visibility' => 'public', + 'isStatic' => false, + 'type' => $param->type, + 'types' => $param->types, + 'shortDescription' => (($pos = strpos($param->description, '.')) !== false) ? + substr($param->description, 0, $pos) : $param->description, + 'description' => $param->description, + 'setter' => $method + ]); + } + } + } + } - /** - * @param ClassDoc $classA - * @param ClassDoc|string $classB - * @return boolean - */ - protected function isSubclassOf($classA, $classB) - { - if (is_object($classB)) { - $classB = $classB->name; - } - if ($classA->name == $classB) { - return true; - } - while ($classA->parentClass !== null && isset($this->classes[$classA->parentClass])) { - $classA = $this->classes[$classA->parentClass]; - if ($classA->name == $classB) { - return true; - } - } - return false; - } + /** + * @param MethodDoc $method + * @param integer $number number of not optional parameters + * @return bool + */ + private function paramsOptional($method, $number = 0) + { + foreach ($method->params as $param) { + if (!$param->isOptional && $number-- <= 0) { + return false; + } + } + + return true; + } + + /** + * @param MethodDoc $method + * @return ParamDoc + */ + private function getFirstNotOptionalParameter($method) + { + foreach ($method->params as $param) { + if (!$param->isOptional) { + return $param; + } + } + + return null; + } + + /** + * @param ClassDoc $classA + * @param ClassDoc|string $classB + * @return boolean + */ + protected function isSubclassOf($classA, $classB) + { + if (is_object($classB)) { + $classB = $classB->name; + } + if ($classA->name == $classB) { + return true; + } + while ($classA->parentClass !== null && isset($this->classes[$classA->parentClass])) { + $classA = $this->classes[$classA->parentClass]; + if ($classA->name == $classB) { + return true; + } + } + + return false; + } } diff --git a/extensions/apidoc/models/EventDoc.php b/extensions/apidoc/models/EventDoc.php index 12edc984109..608dcb4ebe7 100644 --- a/extensions/apidoc/models/EventDoc.php +++ b/extensions/apidoc/models/EventDoc.php @@ -7,7 +7,6 @@ namespace yii\apidoc\models; -use phpDocumentor\Reflection\DocBlock\Tag\ParamTag; use phpDocumentor\Reflection\DocBlock\Tag\ReturnTag; /** @@ -18,35 +17,35 @@ */ class EventDoc extends ConstDoc { - public $type; - public $types; + public $type; + public $types; - /** - * @param \phpDocumentor\Reflection\ClassReflector\ConstantReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + /** + * @param \phpDocumentor\Reflection\ClassReflector\ConstantReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - if ($reflector === null) { - return; - } + if ($reflector === null) { + return; + } - foreach ($this->tags as $i => $tag) { - if ($tag->getName() == 'event') { - $eventTag = new ReturnTag('event', $tag->getContent(), $tag->getDocBlock(), $tag->getLocation()); - $this->type = $eventTag->getType(); - $this->types = $eventTag->getTypes(); - $this->description = ucfirst($eventTag->getDescription()); - if (($pos = strpos($this->description, '.')) !== false) { - $this->shortDescription = substr($this->description, 0, $pos); - } else { - $this->shortDescription = $this->description; - } - unset($this->tags[$i]); - } - } - } + foreach ($this->tags as $i => $tag) { + if ($tag->getName() == 'event') { + $eventTag = new ReturnTag('event', $tag->getContent(), $tag->getDocBlock(), $tag->getLocation()); + $this->type = $eventTag->getType(); + $this->types = $eventTag->getTypes(); + $this->description = ucfirst($eventTag->getDescription()); + if (($pos = strpos($this->description, '.')) !== false) { + $this->shortDescription = substr($this->description, 0, $pos); + } else { + $this->shortDescription = $this->description; + } + unset($this->tags[$i]); + } + } + } } diff --git a/extensions/apidoc/models/FunctionDoc.php b/extensions/apidoc/models/FunctionDoc.php index 5e6e945fbd2..e77c8e43bfc 100644 --- a/extensions/apidoc/models/FunctionDoc.php +++ b/extensions/apidoc/models/FunctionDoc.php @@ -20,62 +20,62 @@ */ class FunctionDoc extends BaseDoc { - /** - * @var ParamDoc[] - */ - public $params = []; - public $exceptions = []; - public $return; - public $returnType; - public $returnTypes; - public $isReturnByReference; + /** + * @var ParamDoc[] + */ + public $params = []; + public $exceptions = []; + public $return; + public $returnType; + public $returnTypes; + public $isReturnByReference; - /** - * @param \phpDocumentor\Reflection\FunctionReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + /** + * @param \phpDocumentor\Reflection\FunctionReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - if ($reflector === null) { - return; - } + if ($reflector === null) { + return; + } - $this->isReturnByReference = $reflector->isByRef(); + $this->isReturnByReference = $reflector->isByRef(); - foreach ($reflector->getArguments() as $arg) { - $arg = new ParamDoc($arg, $context, ['sourceFile' => $this->sourceFile]); - $this->params[$arg->name] = $arg; - } + foreach ($reflector->getArguments() as $arg) { + $arg = new ParamDoc($arg, $context, ['sourceFile' => $this->sourceFile]); + $this->params[$arg->name] = $arg; + } - foreach ($this->tags as $i => $tag) { - if ($tag instanceof ThrowsTag) { - $this->exceptions[$tag->getType()] = $tag->getDescription(); - unset($this->tags[$i]); - } elseif ($tag instanceof PropertyTag) { - // ignore property tag - } elseif ($tag instanceof ParamTag) { - $paramName = $tag->getVariableName(); - if (!isset($this->params[$paramName]) && $context !== null) { - $context->errors[] = [ - 'line' => $this->startLine, - 'file' => $this->sourceFile, - 'message' => "Undefined parameter documented: $paramName in {$this->name}().", - ]; - continue; - } - $this->params[$paramName]->description = ucfirst($tag->getDescription()); - $this->params[$paramName]->type = $tag->getType(); - $this->params[$paramName]->types = $tag->getTypes(); - unset($this->tags[$i]); - } elseif ($tag instanceof ReturnTag) { - $this->returnType = $tag->getType(); - $this->returnTypes = $tag->getTypes(); - $this->return = ucfirst($tag->getDescription()); - unset($this->tags[$i]); - } - } - } + foreach ($this->tags as $i => $tag) { + if ($tag instanceof ThrowsTag) { + $this->exceptions[$tag->getType()] = $tag->getDescription(); + unset($this->tags[$i]); + } elseif ($tag instanceof PropertyTag) { + // ignore property tag + } elseif ($tag instanceof ParamTag) { + $paramName = $tag->getVariableName(); + if (!isset($this->params[$paramName]) && $context !== null) { + $context->errors[] = [ + 'line' => $this->startLine, + 'file' => $this->sourceFile, + 'message' => "Undefined parameter documented: $paramName in {$this->name}().", + ]; + continue; + } + $this->params[$paramName]->description = ucfirst($tag->getDescription()); + $this->params[$paramName]->type = $tag->getType(); + $this->params[$paramName]->types = $tag->getTypes(); + unset($this->tags[$i]); + } elseif ($tag instanceof ReturnTag) { + $this->returnType = $tag->getType(); + $this->returnTypes = $tag->getTypes(); + $this->return = ucfirst($tag->getDescription()); + unset($this->tags[$i]); + } + } + } } diff --git a/extensions/apidoc/models/InterfaceDoc.php b/extensions/apidoc/models/InterfaceDoc.php index 32cbd8384f0..e7969118057 100644 --- a/extensions/apidoc/models/InterfaceDoc.php +++ b/extensions/apidoc/models/InterfaceDoc.php @@ -15,33 +15,33 @@ */ class InterfaceDoc extends TypeDoc { - public $parentInterfaces = []; - - // will be set by Context::updateReferences() - public $implementedBy = []; - - /** - * @param \phpDocumentor\Reflection\InterfaceReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); - - if ($reflector === null) { - return; - } - - foreach ($reflector->getParentInterfaces() as $interface) { - $this->parentInterfaces[] = ltrim($interface, '\\'); - } - - foreach ($this->methods as $method) { - $method->isAbstract = true; - } - - // interface can not have properties - $this->properties = null; - } + public $parentInterfaces = []; + + // will be set by Context::updateReferences() + public $implementedBy = []; + + /** + * @param \phpDocumentor\Reflection\InterfaceReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); + + if ($reflector === null) { + return; + } + + foreach ($reflector->getParentInterfaces() as $interface) { + $this->parentInterfaces[] = ltrim($interface, '\\'); + } + + foreach ($this->methods as $method) { + $method->isAbstract = true; + } + + // interface can not have properties + $this->properties = null; + } } diff --git a/extensions/apidoc/models/MethodDoc.php b/extensions/apidoc/models/MethodDoc.php index 36bdc4e3c07..9d19800009c 100644 --- a/extensions/apidoc/models/MethodDoc.php +++ b/extensions/apidoc/models/MethodDoc.php @@ -15,33 +15,33 @@ */ class MethodDoc extends FunctionDoc { - public $isAbstract; - public $isFinal; + public $isAbstract; + public $isFinal; - public $isStatic; + public $isStatic; - public $visibility; + public $visibility; - // will be set by creating class - public $definedBy; + // will be set by creating class + public $definedBy; - /** - * @param \phpDocumentor\Reflection\ClassReflector\MethodReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + /** + * @param \phpDocumentor\Reflection\ClassReflector\MethodReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - if ($reflector === null) { - return; - } + if ($reflector === null) { + return; + } - $this->isAbstract = $reflector->isAbstract(); - $this->isFinal = $reflector->isFinal(); - $this->isStatic = $reflector->isStatic(); + $this->isAbstract = $reflector->isAbstract(); + $this->isFinal = $reflector->isFinal(); + $this->isStatic = $reflector->isStatic(); - $this->visibility = $reflector->getVisibility(); - } + $this->visibility = $reflector->getVisibility(); + } } diff --git a/extensions/apidoc/models/ParamDoc.php b/extensions/apidoc/models/ParamDoc.php index 69af05ea603..e7830ee4e2f 100644 --- a/extensions/apidoc/models/ParamDoc.php +++ b/extensions/apidoc/models/ParamDoc.php @@ -18,39 +18,39 @@ */ class ParamDoc extends Object { - public $name; - public $typeHint; - public $isOptional; - public $defaultValue; - public $isPassedByReference; - - // will be set by creating class - public $description; - public $type; - public $types; - public $sourceFile; - - /** - * @param \phpDocumentor\Reflection\FunctionReflector\ArgumentReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($config); - - if ($reflector === null) { - return; - } - - $this->name = $reflector->getName(); - $this->typeHint = $reflector->getType(); - $this->isOptional = $reflector->getDefault() !== null; - - // bypass $reflector->getDefault() for short array syntax - if ($reflector->getNode()->default) { - $this->defaultValue = PrettyPrinter::getRepresentationOfValue($reflector->getNode()->default); - } - $this->isPassedByReference = $reflector->isByRef(); - } + public $name; + public $typeHint; + public $isOptional; + public $defaultValue; + public $isPassedByReference; + + // will be set by creating class + public $description; + public $type; + public $types; + public $sourceFile; + + /** + * @param \phpDocumentor\Reflection\FunctionReflector\ArgumentReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($config); + + if ($reflector === null) { + return; + } + + $this->name = $reflector->getName(); + $this->typeHint = $reflector->getType(); + $this->isOptional = $reflector->getDefault() !== null; + + // bypass $reflector->getDefault() for short array syntax + if ($reflector->getNode()->default) { + $this->defaultValue = PrettyPrinter::getRepresentationOfValue($reflector->getNode()->default); + } + $this->isPassedByReference = $reflector->isByRef(); + } } diff --git a/extensions/apidoc/models/PropertyDoc.php b/extensions/apidoc/models/PropertyDoc.php index 0fbb6ca25b1..905b82818fd 100644 --- a/extensions/apidoc/models/PropertyDoc.php +++ b/extensions/apidoc/models/PropertyDoc.php @@ -18,69 +18,69 @@ */ class PropertyDoc extends BaseDoc { - public $visibility; - public $isStatic; + public $visibility; + public $isStatic; - public $type; - public $types; - public $defaultValue; + public $type; + public $types; + public $defaultValue; - // will be set by creating class - public $getter; - public $setter; + // will be set by creating class + public $getter; + public $setter; - // will be set by creating class - public $definedBy; + // will be set by creating class + public $definedBy; - public function getIsReadOnly() - { - return $this->getter !== null && $this->setter === null; - } + public function getIsReadOnly() + { + return $this->getter !== null && $this->setter === null; + } - public function getIsWriteOnly() - { - return $this->getter === null && $this->setter !== null; - } + public function getIsWriteOnly() + { + return $this->getter === null && $this->setter !== null; + } - /** - * @param \phpDocumentor\Reflection\ClassReflector\PropertyReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + /** + * @param \phpDocumentor\Reflection\ClassReflector\PropertyReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - if ($reflector === null) { - return; - } + if ($reflector === null) { + return; + } - $this->visibility = $reflector->getVisibility(); - $this->isStatic = $reflector->isStatic(); + $this->visibility = $reflector->getVisibility(); + $this->isStatic = $reflector->isStatic(); - // bypass $reflector->getDefault() for short array syntax - if ($reflector->getNode()->default) { - $this->defaultValue = PrettyPrinter::getRepresentationOfValue($reflector->getNode()->default); - } + // bypass $reflector->getDefault() for short array syntax + if ($reflector->getNode()->default) { + $this->defaultValue = PrettyPrinter::getRepresentationOfValue($reflector->getNode()->default); + } - foreach ($this->tags as $tag) { - if ($tag instanceof VarTag) { - $this->type = $tag->getType(); - $this->types = $tag->getTypes(); - $this->description = ucfirst($tag->getDescription()); - if (($pos = strpos($this->description, '.')) !== false) { - $this->shortDescription = substr($this->description, 0, $pos + 1); - } else { - $this->shortDescription = $this->description; - } - } - } - if (empty($this->shortDescription) && $context !== null) { - $context->errors[] = [ - 'line' => $this->startLine, - 'file' => $this->sourceFile, - 'message' => "No short description for element '{$this->name}'", - ]; - } - } + foreach ($this->tags as $tag) { + if ($tag instanceof VarTag) { + $this->type = $tag->getType(); + $this->types = $tag->getTypes(); + $this->description = ucfirst($tag->getDescription()); + if (($pos = strpos($this->description, '.')) !== false) { + $this->shortDescription = substr($this->description, 0, $pos + 1); + } else { + $this->shortDescription = $this->description; + } + } + } + if (empty($this->shortDescription) && $context !== null) { + $context->errors[] = [ + 'line' => $this->startLine, + 'file' => $this->sourceFile, + 'message' => "No short description for element '{$this->name}'", + ]; + } + } } diff --git a/extensions/apidoc/models/TraitDoc.php b/extensions/apidoc/models/TraitDoc.php index 3742bfa11e0..981ad9c2264 100644 --- a/extensions/apidoc/models/TraitDoc.php +++ b/extensions/apidoc/models/TraitDoc.php @@ -15,27 +15,27 @@ */ class TraitDoc extends TypeDoc { - // classes using the trait - // will be set by Context::updateReferences() - public $usedBy = []; + // classes using the trait + // will be set by Context::updateReferences() + public $usedBy = []; - public $traits = []; + public $traits = []; - /** - * @param \phpDocumentor\Reflection\TraitReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); + /** + * @param \phpDocumentor\Reflection\TraitReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); - if ($reflector === null) { - return; - } + if ($reflector === null) { + return; + } - foreach ($reflector->getTraits() as $trait) { - $this->traits[] = ltrim($trait, '\\'); - } - } + foreach ($reflector->getTraits() as $trait) { + $this->traits[] = ltrim($trait, '\\'); + } + } } diff --git a/extensions/apidoc/models/TypeDoc.php b/extensions/apidoc/models/TypeDoc.php index e5756da5bef..77a56de9288 100644 --- a/extensions/apidoc/models/TypeDoc.php +++ b/extensions/apidoc/models/TypeDoc.php @@ -25,169 +25,171 @@ */ class TypeDoc extends BaseDoc { - public $authors = []; - /** - * @var MethodDoc[] - */ - public $methods = []; - /** - * @var PropertyDoc[] - */ - public $properties = []; - - public $namespace; - - - public function findSubject($subjectName) - { - if ($subjectName[0] != '$') { - foreach ($this->methods as $name => $method) { - if (rtrim($subjectName, '()') == $name) { - return $method; - } - } - } - if (substr($subjectName, -2, 2) == '()') { - return null; - } - if ($this->properties === null) { - return null; - } - foreach ($this->properties as $name => $property) { - if (ltrim($subjectName, '$') == ltrim($name, '$')) { - return $property; - } - } - return null; - } - - /** - * @return MethodDoc[] - */ - public function getNativeMethods() - { - return $this->getFilteredMethods(null, $this->name); - } - - /** - * @return MethodDoc[] - */ - public function getPublicMethods() - { - return $this->getFilteredMethods('public'); - } - - /** - * @return MethodDoc[] - */ - public function getProtectedMethods() - { - return $this->getFilteredMethods('protected'); - } - - /** - * @param null $visibility - * @param null $definedBy - * @return MethodDoc[] - */ - private function getFilteredMethods($visibility = null, $definedBy = null) - { - $methods = []; - foreach ($this->methods as $name => $method) { - if ($visibility !== null && $method->visibility != $visibility) { - continue; - } - if ($definedBy !== null && $method->definedBy != $definedBy) { - continue; - } - $methods[$name] = $method; - } - return $methods; - } - - /** - * @return PropertyDoc[] - */ - public function getNativeProperties() - { - return $this->getFilteredProperties(null, $this->name); - } - - /** - * @return PropertyDoc[] - */ - public function getPublicProperties() - { - return $this->getFilteredProperties('public'); - } - - /** - * @return PropertyDoc[] - */ - public function getProtectedProperties() - { - return $this->getFilteredProperties('protected'); - } - - /** - * @param null $visibility - * @param null $definedBy - * @return PropertyDoc[] - */ - private function getFilteredProperties($visibility = null, $definedBy = null) - { - if ($this->properties === null) { - return []; - } - $properties = []; - foreach ($this->properties as $name => $property) { - if ($visibility !== null && $property->visibility != $visibility) { - continue; - } - if ($definedBy !== null && $property->definedBy != $definedBy) { - continue; - } - $properties[$name] = $property; - } - return $properties; - } - - /** - * @param \phpDocumentor\Reflection\InterfaceReflector $reflector - * @param Context $context - * @param array $config - */ - public function __construct($reflector = null, $context = null, $config = []) - { - parent::__construct($reflector, $context, $config); - - $this->namespace = trim(StringHelper::dirname($this->name), '\\'); - - if ($reflector === null) { - return; - } - - foreach ($this->tags as $i => $tag) { - if ($tag instanceof AuthorTag) { - $this->authors[$tag->getAuthorName()] = $tag->getAuthorEmail(); - unset($this->tags[$i]); - } - } - - foreach ($reflector->getProperties() as $propertyReflector) { - if ($propertyReflector->getVisibility() != 'private') { - $property = new PropertyDoc($propertyReflector, $context, ['sourceFile' => $this->sourceFile]); - $property->definedBy = $this->name; - $this->properties[$property->name] = $property; - } - } - - foreach ($reflector->getMethods() as $methodReflector) { - if ($methodReflector->getVisibility() != 'private') { - $method = new MethodDoc($methodReflector, $context, ['sourceFile' => $this->sourceFile]); - $method->definedBy = $this->name; - $this->methods[$method->name] = $method; - } - } - } + public $authors = []; + /** + * @var MethodDoc[] + */ + public $methods = []; + /** + * @var PropertyDoc[] + */ + public $properties = []; + + public $namespace; + + public function findSubject($subjectName) + { + if ($subjectName[0] != '$') { + foreach ($this->methods as $name => $method) { + if (rtrim($subjectName, '()') == $name) { + return $method; + } + } + } + if (substr($subjectName, -2, 2) == '()') { + return null; + } + if ($this->properties === null) { + return null; + } + foreach ($this->properties as $name => $property) { + if (ltrim($subjectName, '$') == ltrim($name, '$')) { + return $property; + } + } + + return null; + } + + /** + * @return MethodDoc[] + */ + public function getNativeMethods() + { + return $this->getFilteredMethods(null, $this->name); + } + + /** + * @return MethodDoc[] + */ + public function getPublicMethods() + { + return $this->getFilteredMethods('public'); + } + + /** + * @return MethodDoc[] + */ + public function getProtectedMethods() + { + return $this->getFilteredMethods('protected'); + } + + /** + * @param null $visibility + * @param null $definedBy + * @return MethodDoc[] + */ + private function getFilteredMethods($visibility = null, $definedBy = null) + { + $methods = []; + foreach ($this->methods as $name => $method) { + if ($visibility !== null && $method->visibility != $visibility) { + continue; + } + if ($definedBy !== null && $method->definedBy != $definedBy) { + continue; + } + $methods[$name] = $method; + } + + return $methods; + } + + /** + * @return PropertyDoc[] + */ + public function getNativeProperties() + { + return $this->getFilteredProperties(null, $this->name); + } + + /** + * @return PropertyDoc[] + */ + public function getPublicProperties() + { + return $this->getFilteredProperties('public'); + } + + /** + * @return PropertyDoc[] + */ + public function getProtectedProperties() + { + return $this->getFilteredProperties('protected'); + } + + /** + * @param null $visibility + * @param null $definedBy + * @return PropertyDoc[] + */ + private function getFilteredProperties($visibility = null, $definedBy = null) + { + if ($this->properties === null) { + return []; + } + $properties = []; + foreach ($this->properties as $name => $property) { + if ($visibility !== null && $property->visibility != $visibility) { + continue; + } + if ($definedBy !== null && $property->definedBy != $definedBy) { + continue; + } + $properties[$name] = $property; + } + + return $properties; + } + + /** + * @param \phpDocumentor\Reflection\InterfaceReflector $reflector + * @param Context $context + * @param array $config + */ + public function __construct($reflector = null, $context = null, $config = []) + { + parent::__construct($reflector, $context, $config); + + $this->namespace = trim(StringHelper::dirname($this->name), '\\'); + + if ($reflector === null) { + return; + } + + foreach ($this->tags as $i => $tag) { + if ($tag instanceof AuthorTag) { + $this->authors[$tag->getAuthorName()] = $tag->getAuthorEmail(); + unset($this->tags[$i]); + } + } + + foreach ($reflector->getProperties() as $propertyReflector) { + if ($propertyReflector->getVisibility() != 'private') { + $property = new PropertyDoc($propertyReflector, $context, ['sourceFile' => $this->sourceFile]); + $property->definedBy = $this->name; + $this->properties[$property->name] = $property; + } + } + + foreach ($reflector->getMethods() as $methodReflector) { + if ($methodReflector->getVisibility() != 'private') { + $method = new MethodDoc($methodReflector, $context, ['sourceFile' => $this->sourceFile]); + $method->definedBy = $this->name; + $this->methods[$method->name] = $method; + } + } + } } diff --git a/extensions/apidoc/renderers/ApiRenderer.php b/extensions/apidoc/renderers/ApiRenderer.php index 1ed06b66636..b082a113da9 100644 --- a/extensions/apidoc/renderers/ApiRenderer.php +++ b/extensions/apidoc/renderers/ApiRenderer.php @@ -8,16 +8,7 @@ namespace yii\apidoc\renderers; use Yii; -use yii\apidoc\models\ClassDoc; -use yii\apidoc\models\ConstDoc; use yii\apidoc\models\Context; -use yii\apidoc\models\EventDoc; -use yii\apidoc\models\InterfaceDoc; -use yii\apidoc\models\MethodDoc; -use yii\apidoc\models\PropertyDoc; -use yii\apidoc\models\TraitDoc; -use yii\base\Component; -use yii\console\Controller; /** * Base class for all API documentation renderers @@ -27,11 +18,11 @@ */ abstract class ApiRenderer extends BaseRenderer { - /** - * Renders a given [[Context]]. - * - * @param Context $context the api documentation context to render. - * @param $targetDir - */ - public abstract function render($context, $targetDir); -} \ No newline at end of file + /** + * Renders a given [[Context]]. + * + * @param Context $context the api documentation context to render. + * @param $targetDir + */ + abstract public function render($context, $targetDir); +} diff --git a/extensions/apidoc/renderers/BaseRenderer.php b/extensions/apidoc/renderers/BaseRenderer.php index 67e1acf8c01..90ea56a63ee 100644 --- a/extensions/apidoc/renderers/BaseRenderer.php +++ b/extensions/apidoc/renderers/BaseRenderer.php @@ -31,171 +31,172 @@ */ abstract class BaseRenderer extends Component { - const GUIDE_PREFIX = 'guide-'; - - public $apiUrl; - /** - * @var Context the [[Context]] currently being rendered. - */ - public $apiContext; - /** - * @var Controller the apidoc controller instance. Can be used to control output. - */ - public $controller; - - public $guideUrl; - public $guideReferences = []; - - - public function init() - { - ApiMarkdown::$renderer = $this; - } - - /** - * creates a link to a type (class, interface or trait) - * @param ClassDoc|InterfaceDoc|TraitDoc|ClassDoc[]|InterfaceDoc[]|TraitDoc[] $types - * @param string $title a title to be used for the link TODO check whether [[yii\...|Class]] is supported - * @param BaseDoc $context - * @param array $options additional HTML attributes for the link. - * @return string - */ - public function createTypeLink($types, $context = null, $title = null, $options = []) - { - if (!is_array($types)) { - $types = [$types]; - } - if (count($types) > 1) { - $title = null; - } - $links = []; - foreach ($types as $type) { - $postfix = ''; - if (!is_object($type)) { - if (substr($type, -2, 2) == '[]') { - $postfix = '[]'; - $type = substr($type, 0, -2); - } - - if (($t = $this->apiContext->getType(ltrim($type, '\\'))) !== null) { - $type = $t; - } elseif ($type[0] !== '\\' && ($t = $this->apiContext->getType($this->resolveNamespace($context) . '\\' . ltrim($type, '\\'))) !== null) { - $type = $t; - } else { - ltrim($type, '\\'); - } - } - if (!is_object($type)) { - $linkText = ltrim($type, '\\'); - if ($title !== null) { - $linkText = $title; - } - $phpTypes = [ - 'callable', - 'array', - 'string', - 'boolean', - 'integer', - 'float', - 'object', - 'resource', - 'null', - ]; - // check if it is PHP internal class - if (((class_exists($type, false) || interface_exists($type, false) || trait_exists($type, false)) && - ($reflection = new \ReflectionClass($type)) && $reflection->isInternal())) { - $links[] = $this->generateLink($linkText, 'http://www.php.net/class.' . strtolower(ltrim($type, '\\')), $options) . $postfix; - } elseif (in_array($type, $phpTypes)) { - $links[] = $this->generateLink($linkText, 'http://www.php.net/language.types.' . strtolower(ltrim($type, '\\')), $options) . $postfix; - } else { - $links[] = $type; - } - } else { - $linkText = $type->name; - if ($title !== null) { - $linkText = $title; - } - $links[] = $this->generateLink($linkText, $this->generateApiUrl($type->name), $options) . $postfix; - } - } - return implode('|', $links); - } - - - /** - * creates a link to a subject - * @param PropertyDoc|MethodDoc|ConstDoc|EventDoc $subject - * @param string $title - * @param array $options additional HTML attributes for the link. - * @return string - */ - public function createSubjectLink($subject, $title = null, $options = []) - { - if ($title === null) { - if ($subject instanceof MethodDoc) { - $title = $subject->name . '()'; - } else { - $title = $subject->name; - } - } - if (($type = $this->apiContext->getType($subject->definedBy)) === null) { - return $subject->name; - } else { - $link = $this->generateApiUrl($type->name); - if ($subject instanceof MethodDoc) { - $link .= '#' . $subject->name . '()'; - } else { - $link .= '#' . $subject->name; - } - $link .= '-detail'; - return $this->generateLink($title, $link, $options); - } - } - - /** - * @param BaseDoc $context - */ - private function resolveNamespace($context) - { - // TODO use phpdoc Context for this - if ($context === null) { - return ''; - } - if ($context instanceof TypeDoc) { - return $context->namespace; - } - if ($context->hasProperty('definedBy')) { - $type = $this->apiContext->getType($context); - if ($type !== null) { - return $type->namespace; - } - } - return ''; - } - - /** - * generate link markup - * @param $text - * @param $href - * @param array $options additional HTML attributes for the link. - * @return mixed - */ - protected abstract function generateLink($text, $href, $options = []); - - /** - * Generate an url to a type in apidocs - * @param $typeName - * @return mixed - */ - public abstract function generateApiUrl($typeName); - - /** - * Generate an url to a guide page - * @param string $file - * @return string - */ - public function generateGuideUrl($file) - { - return rtrim($this->guideUrl, '/') . '/' . static::GUIDE_PREFIX . basename($file, '.md') . '.html'; - } + const GUIDE_PREFIX = 'guide-'; + + public $apiUrl; + /** + * @var Context the [[Context]] currently being rendered. + */ + public $apiContext; + /** + * @var Controller the apidoc controller instance. Can be used to control output. + */ + public $controller; + + public $guideUrl; + public $guideReferences = []; + + public function init() + { + ApiMarkdown::$renderer = $this; + } + + /** + * creates a link to a type (class, interface or trait) + * @param ClassDoc|InterfaceDoc|TraitDoc|ClassDoc[]|InterfaceDoc[]|TraitDoc[] $types + * @param string $title a title to be used for the link TODO check whether [[yii\...|Class]] is supported + * @param BaseDoc $context + * @param array $options additional HTML attributes for the link. + * @return string + */ + public function createTypeLink($types, $context = null, $title = null, $options = []) + { + if (!is_array($types)) { + $types = [$types]; + } + if (count($types) > 1) { + $title = null; + } + $links = []; + foreach ($types as $type) { + $postfix = ''; + if (!is_object($type)) { + if (substr($type, -2, 2) == '[]') { + $postfix = '[]'; + $type = substr($type, 0, -2); + } + + if (($t = $this->apiContext->getType(ltrim($type, '\\'))) !== null) { + $type = $t; + } elseif ($type[0] !== '\\' && ($t = $this->apiContext->getType($this->resolveNamespace($context) . '\\' . ltrim($type, '\\'))) !== null) { + $type = $t; + } else { + ltrim($type, '\\'); + } + } + if (!is_object($type)) { + $linkText = ltrim($type, '\\'); + if ($title !== null) { + $linkText = $title; + } + $phpTypes = [ + 'callable', + 'array', + 'string', + 'boolean', + 'integer', + 'float', + 'object', + 'resource', + 'null', + ]; + // check if it is PHP internal class + if (((class_exists($type, false) || interface_exists($type, false) || trait_exists($type, false)) && + ($reflection = new \ReflectionClass($type)) && $reflection->isInternal())) { + $links[] = $this->generateLink($linkText, 'http://www.php.net/class.' . strtolower(ltrim($type, '\\')), $options) . $postfix; + } elseif (in_array($type, $phpTypes)) { + $links[] = $this->generateLink($linkText, 'http://www.php.net/language.types.' . strtolower(ltrim($type, '\\')), $options) . $postfix; + } else { + $links[] = $type; + } + } else { + $linkText = $type->name; + if ($title !== null) { + $linkText = $title; + } + $links[] = $this->generateLink($linkText, $this->generateApiUrl($type->name), $options) . $postfix; + } + } + + return implode('|', $links); + } + + /** + * creates a link to a subject + * @param PropertyDoc|MethodDoc|ConstDoc|EventDoc $subject + * @param string $title + * @param array $options additional HTML attributes for the link. + * @return string + */ + public function createSubjectLink($subject, $title = null, $options = []) + { + if ($title === null) { + if ($subject instanceof MethodDoc) { + $title = $subject->name . '()'; + } else { + $title = $subject->name; + } + } + if (($type = $this->apiContext->getType($subject->definedBy)) === null) { + return $subject->name; + } else { + $link = $this->generateApiUrl($type->name); + if ($subject instanceof MethodDoc) { + $link .= '#' . $subject->name . '()'; + } else { + $link .= '#' . $subject->name; + } + $link .= '-detail'; + + return $this->generateLink($title, $link, $options); + } + } + + /** + * @param BaseDoc $context + */ + private function resolveNamespace($context) + { + // TODO use phpdoc Context for this + if ($context === null) { + return ''; + } + if ($context instanceof TypeDoc) { + return $context->namespace; + } + if ($context->hasProperty('definedBy')) { + $type = $this->apiContext->getType($context); + if ($type !== null) { + return $type->namespace; + } + } + + return ''; + } + + /** + * generate link markup + * @param $text + * @param $href + * @param array $options additional HTML attributes for the link. + * @return mixed + */ + abstract protected function generateLink($text, $href, $options = []); + + /** + * Generate an url to a type in apidocs + * @param $typeName + * @return mixed + */ + abstract public function generateApiUrl($typeName); + + /** + * Generate an url to a guide page + * @param string $file + * @return string + */ + public function generateGuideUrl($file) + { + return rtrim($this->guideUrl, '/') . '/' . static::GUIDE_PREFIX . basename($file, '.md') . '.html'; + } } diff --git a/extensions/apidoc/renderers/GuideRenderer.php b/extensions/apidoc/renderers/GuideRenderer.php index 5c5d4438a62..386c2615805 100644 --- a/extensions/apidoc/renderers/GuideRenderer.php +++ b/extensions/apidoc/renderers/GuideRenderer.php @@ -8,16 +8,6 @@ namespace yii\apidoc\renderers; use Yii; -use yii\apidoc\models\ClassDoc; -use yii\apidoc\models\ConstDoc; -use yii\apidoc\models\Context; -use yii\apidoc\models\EventDoc; -use yii\apidoc\models\InterfaceDoc; -use yii\apidoc\models\MethodDoc; -use yii\apidoc\models\PropertyDoc; -use yii\apidoc\models\TraitDoc; -use yii\base\Component; -use yii\console\Controller; /** * Base class for all Guide documentation renderers @@ -27,12 +17,12 @@ */ abstract class GuideRenderer extends BaseRenderer { - /** - * Render markdown files - * - * @param array $files list of markdown files to render - * @param $targetDir - */ - public abstract function render($files, $targetDir); + /** + * Render markdown files + * + * @param array $files list of markdown files to render + * @param $targetDir + */ + abstract public function render($files, $targetDir); -} \ No newline at end of file +} diff --git a/extensions/apidoc/templates/bootstrap/ApiRenderer.php b/extensions/apidoc/templates/bootstrap/ApiRenderer.php index bd7028f16c7..e04f5f6d969 100644 --- a/extensions/apidoc/templates/bootstrap/ApiRenderer.php +++ b/extensions/apidoc/templates/bootstrap/ApiRenderer.php @@ -6,15 +6,7 @@ */ namespace yii\apidoc\templates\bootstrap; -use yii\apidoc\helpers\ApiMarkdown; -use yii\apidoc\models\ClassDoc; -use yii\apidoc\models\ConstDoc; use yii\apidoc\models\Context; -use yii\apidoc\models\EventDoc; -use yii\apidoc\models\InterfaceDoc; -use yii\apidoc\models\MethodDoc; -use yii\apidoc\models\PropertyDoc; -use yii\apidoc\models\TraitDoc; use yii\console\Controller; use Yii; use yii\helpers\Console; @@ -27,90 +19,89 @@ */ class ApiRenderer extends \yii\apidoc\templates\html\ApiRenderer { - use RendererTrait; + use RendererTrait; - public $layout = '@yii/apidoc/templates/bootstrap/layouts/api.php'; - public $indexView = '@yii/apidoc/templates/bootstrap/views/index.php'; + public $layout = '@yii/apidoc/templates/bootstrap/layouts/api.php'; + public $indexView = '@yii/apidoc/templates/bootstrap/views/index.php'; - /** - * @inheritdoc - */ - public function render($context, $targetDir) - { - $types = array_merge($context->classes, $context->interfaces, $context->traits); + /** + * @inheritdoc + */ + public function render($context, $targetDir) + { + $types = array_merge($context->classes, $context->interfaces, $context->traits); - $extTypes = []; - foreach ($this->extensions as $k => $ext) { - $extType = $this->filterTypes($types, $ext); - if (empty($extType)) { - unset($this->extensions[$k]); - continue; - } - $extTypes[$ext] = $extType; - } + $extTypes = []; + foreach ($this->extensions as $k => $ext) { + $extType = $this->filterTypes($types, $ext); + if (empty($extType)) { + unset($this->extensions[$k]); + continue; + } + $extTypes[$ext] = $extType; + } - // render view files - parent::render($context, $targetDir); + // render view files + parent::render($context, $targetDir); - if ($this->controller !== null) { - $this->controller->stdout('generating extension index files...'); - } + if ($this->controller !== null) { + $this->controller->stdout('generating extension index files...'); + } - foreach ($extTypes as $ext => $extType) { - $readme = @file_get_contents("https://raw.github.com/yiisoft/yii2-$ext/master/README.md"); - $indexFileContent = $this->renderWithLayout($this->indexView, [ - 'docContext' => $context, - 'types' => $extType, - 'readme' => $readme ?: null, - ]); - file_put_contents($targetDir . "/ext-{$ext}-index.html", $indexFileContent); - } + foreach ($extTypes as $ext => $extType) { + $readme = @file_get_contents("https://raw.github.com/yiisoft/yii2-$ext/master/README.md"); + $indexFileContent = $this->renderWithLayout($this->indexView, [ + 'docContext' => $context, + 'types' => $extType, + 'readme' => $readme ?: null, + ]); + file_put_contents($targetDir . "/ext-{$ext}-index.html", $indexFileContent); + } - $yiiTypes = $this->filterTypes($types, 'yii'); - if (empty($yiiTypes)) { + $yiiTypes = $this->filterTypes($types, 'yii'); + if (empty($yiiTypes)) { // $readme = @file_get_contents("https://raw.github.com/yiisoft/yii2-framework/master/README.md"); - $indexFileContent = $this->renderWithLayout($this->indexView, [ - 'docContext' => $context, - 'types' => $this->filterTypes($types, 'app'), - 'readme' => null, - ]); - } else { - $readme = @file_get_contents("https://raw.github.com/yiisoft/yii2-framework/master/README.md"); - $indexFileContent = $this->renderWithLayout($this->indexView, [ - 'docContext' => $context, - 'types' => $yiiTypes, - 'readme' => $readme ?: null, - ]); - } - file_put_contents($targetDir . '/index.html', $indexFileContent); + $indexFileContent = $this->renderWithLayout($this->indexView, [ + 'docContext' => $context, + 'types' => $this->filterTypes($types, 'app'), + 'readme' => null, + ]); + } else { + $readme = @file_get_contents("https://raw.github.com/yiisoft/yii2-framework/master/README.md"); + $indexFileContent = $this->renderWithLayout($this->indexView, [ + 'docContext' => $context, + 'types' => $yiiTypes, + 'readme' => $readme ?: null, + ]); + } + file_put_contents($targetDir . '/index.html', $indexFileContent); - if ($this->controller !== null) { - $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); - } - } + if ($this->controller !== null) { + $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); + } + } - public function getSourceUrl($type, $line = null) - { - if (is_string($type)) { - $type = $this->apiContext->getType($type); - } + public function getSourceUrl($type, $line = null) + { + if (is_string($type)) { + $type = $this->apiContext->getType($type); + } - $baseUrl = 'https://github.com/yiisoft/yii2/blob/master'; - switch ($this->getTypeCategory($type)) - { - case 'yii': - $url = '/framework/' . str_replace('\\', '/', substr($type->name, 4)) . '.php'; - break; - case 'app': - return null; - default: - $url = '/extensions/' . str_replace('\\', '/', substr($type->name, 4)) . '.php'; - break; - } + $baseUrl = 'https://github.com/yiisoft/yii2/blob/master'; + switch ($this->getTypeCategory($type)) { + case 'yii': + $url = '/framework/' . str_replace('\\', '/', substr($type->name, 4)) . '.php'; + break; + case 'app': + return null; + default: + $url = '/extensions/' . str_replace('\\', '/', substr($type->name, 4)) . '.php'; + break; + } - if ($line === null) - return $baseUrl . $url; - else - return $baseUrl . $url . '#L' . $line; - } + if ($line === null) + return $baseUrl . $url; + else + return $baseUrl . $url . '#L' . $line; + } } diff --git a/extensions/apidoc/templates/bootstrap/GuideRenderer.php b/extensions/apidoc/templates/bootstrap/GuideRenderer.php index 95b9004aac3..e31ec4d0819 100644 --- a/extensions/apidoc/templates/bootstrap/GuideRenderer.php +++ b/extensions/apidoc/templates/bootstrap/GuideRenderer.php @@ -6,18 +6,7 @@ */ namespace yii\apidoc\templates\bootstrap; -use yii\apidoc\helpers\ApiMarkdown; -use yii\apidoc\models\ClassDoc; -use yii\apidoc\models\ConstDoc; -use yii\apidoc\models\Context; -use yii\apidoc\models\EventDoc; -use yii\apidoc\models\InterfaceDoc; -use yii\apidoc\models\MethodDoc; -use yii\apidoc\models\PropertyDoc; -use yii\apidoc\models\TraitDoc; -use yii\console\Controller; use Yii; -use yii\helpers\Console; use yii\helpers\Html; /** @@ -27,27 +16,27 @@ */ class GuideRenderer extends \yii\apidoc\templates\html\GuideRenderer { - use RendererTrait; + use RendererTrait; - public $layout = '@yii/apidoc/templates/bootstrap/layouts/guide.php'; + public $layout = '@yii/apidoc/templates/bootstrap/layouts/guide.php'; - /** - * @inheritDoc - */ - public function render($files, $targetDir) - { - $types = array_merge($this->apiContext->classes, $this->apiContext->interfaces, $this->apiContext->traits); + /** + * @inheritDoc + */ + public function render($files, $targetDir) + { + $types = array_merge($this->apiContext->classes, $this->apiContext->interfaces, $this->apiContext->traits); - $extTypes = []; - foreach ($this->extensions as $k => $ext) { - $extType = $this->filterTypes($types, $ext); - if (empty($extType)) { - unset($this->extensions[$k]); - continue; - } - $extTypes[$ext] = $extType; - } + $extTypes = []; + foreach ($this->extensions as $k => $ext) { + $extType = $this->filterTypes($types, $ext); + if (empty($extType)) { + unset($this->extensions[$k]); + continue; + } + $extTypes[$ext] = $extType; + } - parent::render($files, $targetDir); - } + parent::render($files, $targetDir); + } } diff --git a/extensions/apidoc/templates/bootstrap/RendererTrait.php b/extensions/apidoc/templates/bootstrap/RendererTrait.php index 74f2fe2cb4c..376aa212ae1 100644 --- a/extensions/apidoc/templates/bootstrap/RendererTrait.php +++ b/extensions/apidoc/templates/bootstrap/RendererTrait.php @@ -5,85 +5,87 @@ namespace yii\apidoc\templates\bootstrap; - trait RendererTrait { - public $extensions = [ - 'apidoc', - 'authclient', - 'bootstrap', - 'codeception', - 'composer', - 'debug', - 'elasticsearch', - 'faker', - 'gii', - 'imagine', - 'jui', - 'mongodb', - 'redis', - 'smarty', - 'sphinx', - 'swiftmailer', - 'twig', - ]; + public $extensions = [ + 'apidoc', + 'authclient', + 'bootstrap', + 'codeception', + 'composer', + 'debug', + 'elasticsearch', + 'faker', + 'gii', + 'imagine', + 'jui', + 'mongodb', + 'redis', + 'smarty', + 'sphinx', + 'swiftmailer', + 'twig', + ]; + + public function getNavTypes($type, $types) + { + if ($type === null) { + return $types; + } + + return $this->filterTypes($types, $this->getTypeCategory($type)); + } + + protected function getTypeCategory($type) + { + $extensions = $this->extensions; + $navClasses = 'app'; + if (isset($type)) { + if ($type->name == 'Yii') { + $navClasses = 'yii'; + } elseif (strncmp($type->name, 'yii\\', 4) == 0) { + $navClasses = 'yii'; + $subName = substr($type->name, 4); + if (($pos = strpos($subName, '\\')) !== false) { + $subNamespace = substr($subName, 0, $pos); + if (in_array($subNamespace, $extensions)) { + $navClasses = $subNamespace; + } + } + } + } + + return $navClasses; + } - public function getNavTypes($type, $types) - { - if ($type === null) { - return $types; - } - return $this->filterTypes($types, $this->getTypeCategory($type)); - } + protected function filterTypes($types, $navClasses) + { + switch ($navClasses) { + case 'app': + $types = array_filter($types, function ($val) { + return strncmp($val->name, 'yii\\', 4) !== 0; + }); + break; + case 'yii': + $self = $this; + $types = array_filter($types, function ($val) use ($self) { + if ($val->name == 'Yii') { + return true; + } + if (strlen($val->name) < 5) { + return false; + } + $subName = substr($val->name, 4, strpos($val->name, '\\', 5) - 4); - protected function getTypeCategory($type) - { - $extensions = $this->extensions; - $navClasses = 'app'; - if (isset($type)) { - if ($type->name == 'Yii') { - $navClasses = 'yii'; - } elseif (strncmp($type->name, 'yii\\', 4) == 0) { - $navClasses = 'yii'; - $subName = substr($type->name, 4); - if (($pos = strpos($subName, '\\')) !== false) { - $subNamespace = substr($subName, 0, $pos); - if (in_array($subNamespace, $extensions)) { - $navClasses = $subNamespace; - } - } - } - } - return $navClasses; - } + return strncmp($val->name, 'yii\\', 4) === 0 && !in_array($subName, $self->extensions); + }); + break; + default: + $types = array_filter($types, function ($val) use ($navClasses) { + return strncmp($val->name, "yii\\$navClasses\\", strlen("yii\\$navClasses\\")) === 0; + }); + } - protected function filterTypes($types, $navClasses) - { - switch ($navClasses) - { - case 'app': - $types = array_filter($types, function($val) { - return strncmp($val->name, 'yii\\', 4) !== 0; - }); - break; - case 'yii': - $self = $this; - $types = array_filter($types, function($val) use ($self) { - if ($val->name == 'Yii') { - return true; - } - if (strlen($val->name) < 5) { - return false; - } - $subName = substr($val->name, 4, strpos($val->name, '\\', 5) - 4); - return strncmp($val->name, 'yii\\', 4) === 0 && !in_array($subName, $self->extensions); - }); - break; - default: - $types = array_filter($types, function($val) use ($navClasses) { - return strncmp($val->name, "yii\\$navClasses\\", strlen("yii\\$navClasses\\")) === 0; - }); - } - return $types; - } -} \ No newline at end of file + return $types; + } +} diff --git a/extensions/apidoc/templates/bootstrap/SideNavWidget.php b/extensions/apidoc/templates/bootstrap/SideNavWidget.php index fb8f9636f6f..294141c83b6 100644 --- a/extensions/apidoc/templates/bootstrap/SideNavWidget.php +++ b/extensions/apidoc/templates/bootstrap/SideNavWidget.php @@ -50,132 +50,131 @@ */ class SideNavWidget extends \yii\bootstrap\Widget { - /** - * @var array list of items in the nav widget. Each array element represents a single - * menu item which can be either a string or an array with the following structure: - * - * - label: string, required, the nav item label. - * - url: optional, the item's URL. Defaults to "#". - * - visible: boolean, optional, whether this menu item is visible. Defaults to true. - * - linkOptions: array, optional, the HTML attributes of the item's link. - * - options: array, optional, the HTML attributes of the item container (LI). - * - active: boolean, optional, whether the item should be on active state or not. - * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, - * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. - * - * If a menu item is a string, it will be rendered directly without HTML encoding. - */ - public $items = []; - /** - * @var boolean whether the nav items labels should be HTML-encoded. - */ - public $encodeLabels = true; - /** - * @var string the route used to determine if a menu item is active or not. - * If not set, it will use the route of the current request. - * @see params - * @see isItemActive - */ - public $activeUrl; + /** + * @var array list of items in the nav widget. Each array element represents a single + * menu item which can be either a string or an array with the following structure: + * + * - label: string, required, the nav item label. + * - url: optional, the item's URL. Defaults to "#". + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - linkOptions: array, optional, the HTML attributes of the item's link. + * - options: array, optional, the HTML attributes of the item container (LI). + * - active: boolean, optional, whether the item should be on active state or not. + * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, + * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. + * + * If a menu item is a string, it will be rendered directly without HTML encoding. + */ + public $items = []; + /** + * @var boolean whether the nav items labels should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var string the route used to determine if a menu item is active or not. + * If not set, it will use the route of the current request. + * @see params + * @see isItemActive + */ + public $activeUrl; + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + if (!isset($this->options['class'])) { + Html::addCssClass($this->options, 'list-group'); + } + } - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - if (!isset($this->options['class'])) { - Html::addCssClass($this->options, 'list-group'); - } - } + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems(); + BootstrapAsset::register($this->getView()); + } - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderItems(); - BootstrapAsset::register($this->getView()); - } + /** + * Renders widget items. + */ + public function renderItems() + { + $items = []; + foreach ($this->items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + $items[] = $this->renderItem($item, count($this->items) !== 1); + } - /** - * Renders widget items. - */ - public function renderItems() - { - $items = []; - foreach ($this->items as $i => $item) { - if (isset($item['visible']) && !$item['visible']) { - unset($items[$i]); - continue; - } - $items[] = $this->renderItem($item, count($this->items) !== 1); - } + return Html::tag('div', implode("\n", $items), $this->options); + } - return Html::tag('div', implode("\n", $items), $this->options); - } + /** + * Renders a widget's item. + * @param string|array $item the item to render. + * @param boolean $collapsed whether to collapse item if not active + * @throws \yii\base\InvalidConfigException + * @return string the rendering result. + * @throws InvalidConfigException if label is not defined + */ + public function renderItem($item, $collapsed = true) + { + if (is_string($item)) { + return $item; + } + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } - /** - * Renders a widget's item. - * @param string|array $item the item to render. - * @param boolean $collapsed whether to collapse item if not active - * @throws \yii\base\InvalidConfigException - * @return string the rendering result. - * @throws InvalidConfigException if label is not defined - */ - public function renderItem($item, $collapsed = true) - { - if (is_string($item)) { - return $item; - } - if (!isset($item['label'])) { - throw new InvalidConfigException("The 'label' option is required."); - } - - $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; // $options = ArrayHelper::getValue($item, 'options', []); - $items = ArrayHelper::getValue($item, 'items'); - $url = Url::to(ArrayHelper::getValue($item, 'url', '#')); - $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); - Html::addCssClass($linkOptions, 'list-group-item'); + $items = ArrayHelper::getValue($item, 'items'); + $url = Url::to(ArrayHelper::getValue($item, 'url', '#')); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); + Html::addCssClass($linkOptions, 'list-group-item'); - if (isset($item['active'])) { - $active = ArrayHelper::remove($item, 'active', false); - } else { - $active = ($url == $this->activeUrl); - } + if (isset($item['active'])) { + $active = ArrayHelper::remove($item, 'active', false); + } else { + $active = ($url == $this->activeUrl); + } - if ($items !== null) { - $linkOptions['data-toggle'] = 'collapse'; - $linkOptions['data-parent'] = '#' . $this->id; - $id = $this->id . '-' . static::$counter++; - $url = '#' . $id; - $label .= ' ' . Html::tag('b', '', ['class' => 'caret']); - if (is_array($items)) { - if ($active === false) { - foreach ($items as $subItem) { - if (isset($subItem['active']) && $subItem['active']) { - $active = true; - } - } - } - $items = static::widget([ - 'id' => $id, - 'items' => $items, - 'encodeLabels' => $this->encodeLabels, - 'view' => $this->getView(), - 'options' => [ - 'class' => "submenu panel-collapse collapse" . ($active || !$collapsed ? ' in' : '') - ] - ]); - } - } + if ($items !== null) { + $linkOptions['data-toggle'] = 'collapse'; + $linkOptions['data-parent'] = '#' . $this->id; + $id = $this->id . '-' . static::$counter++; + $url = '#' . $id; + $label .= ' ' . Html::tag('b', '', ['class' => 'caret']); + if (is_array($items)) { + if ($active === false) { + foreach ($items as $subItem) { + if (isset($subItem['active']) && $subItem['active']) { + $active = true; + } + } + } + $items = static::widget([ + 'id' => $id, + 'items' => $items, + 'encodeLabels' => $this->encodeLabels, + 'view' => $this->getView(), + 'options' => [ + 'class' => "submenu panel-collapse collapse" . ($active || !$collapsed ? ' in' : '') + ] + ]); + } + } - if ($active) { - Html::addCssClass($linkOptions, 'active'); - } + if ($active) { + Html::addCssClass($linkOptions, 'active'); + } - return Html::a($label, $url, $linkOptions) . $items; - } + return Html::a($label, $url, $linkOptions) . $items; + } } diff --git a/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php b/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php index 0a377074b58..9a3b01c1380 100644 --- a/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php +++ b/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php @@ -17,17 +17,17 @@ */ class AssetBundle extends \yii\web\AssetBundle { - public $sourcePath = '@yii/apidoc/templates/bootstrap/assets/css'; - public $css = [ + public $sourcePath = '@yii/apidoc/templates/bootstrap/assets/css'; + public $css = [ // 'api.css', - 'style.css', - ]; - public $depends = [ - 'yii\web\JqueryAsset', - 'yii\bootstrap\BootstrapAsset', - 'yii\bootstrap\BootstrapPluginAsset', - ]; - public $jsOptions = [ - 'position' => View::POS_HEAD, - ]; + 'style.css', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + 'yii\bootstrap\BootstrapAsset', + 'yii\bootstrap\BootstrapPluginAsset', + ]; + public $jsOptions = [ + 'position' => View::POS_HEAD, + ]; } diff --git a/extensions/apidoc/templates/bootstrap/layouts/api.php b/extensions/apidoc/templates/bootstrap/layouts/api.php index b7a5291f3dd..7cc38b182d5 100644 --- a/extensions/apidoc/templates/bootstrap/layouts/api.php +++ b/extensions/apidoc/templates/bootstrap/layouts/api.php @@ -15,68 +15,69 @@ $this->beginContent('@yii/apidoc/templates/bootstrap/layouts/main.php'); ?>
-
- getNavTypes(isset($type) ? $type : null, $types); - ksort($types); - $nav = []; - foreach ($types as $i => $class) { - $namespace = $class->namespace; - if (empty($namespace)) { - $namespace = 'Not namespaced classes'; - } - if (!isset($nav[$namespace])) { - $nav[$namespace] = [ - 'label' => $namespace, - 'url' => '#', - 'items' => [], - ]; - } - $nav[$namespace]['items'][] = [ - 'label' => StringHelper::basename($class->name), - 'url' => './' . $renderer->generateApiUrl($class->name), - 'active' => isset($type) && ($class->name == $type->name), - ]; - } ?> - 'navigation', - 'items' => $nav, - 'view' => $this, - ])?> -
-
- -
+
+ getNavTypes(isset($type) ? $type : null, $types); + ksort($types); + $nav = []; + foreach ($types as $i => $class) { + $namespace = $class->namespace; + if (empty($namespace)) { + $namespace = 'Not namespaced classes'; + } + if (!isset($nav[$namespace])) { + $nav[$namespace] = [ + 'label' => $namespace, + 'url' => '#', + 'items' => [], + ]; + } + $nav[$namespace]['items'][] = [ + 'label' => StringHelper::basename($class->name), + 'url' => './' . $renderer->generateApiUrl($class->name), + 'active' => isset($type) && ($class->name == $type->name), + ]; + } ?> + 'navigation', + 'items' => $nav, + 'view' => $this, + ])?> +
+
+ +
-endContent(); ?> \ No newline at end of file +endContent(); ?> diff --git a/extensions/apidoc/templates/bootstrap/layouts/guide.php b/extensions/apidoc/templates/bootstrap/layouts/guide.php index 941dc7002bb..7ef2491d032 100644 --- a/extensions/apidoc/templates/bootstrap/layouts/guide.php +++ b/extensions/apidoc/templates/bootstrap/layouts/guide.php @@ -10,31 +10,31 @@ $this->beginContent('@yii/apidoc/templates/bootstrap/layouts/main.php'); ?>
-
- 'Index', - 'url' => $this->context->generateGuideUrl('index.md'), - 'active' => isset($currentFile) && (basename($currentFile) == 'index.md'), - ]; - foreach ($headlines as $file => $headline) { - $nav[] = [ - 'label' => $headline, - 'url' => $this->context->generateGuideUrl($file), - 'active' => isset($currentFile) && ($file == $currentFile), - ]; - } ?> - 'navigation', - 'items' => $nav, - 'view' => $this, - ]) ?> -
-
- -
+
+ 'Index', + 'url' => $this->context->generateGuideUrl('index.md'), + 'active' => isset($currentFile) && (basename($currentFile) == 'index.md'), + ]; + foreach ($headlines as $file => $headline) { + $nav[] = [ + 'label' => $headline, + 'url' => $this->context->generateGuideUrl($file), + 'active' => isset($currentFile) && ($file == $currentFile), + ]; + } ?> + 'navigation', + 'items' => $nav, + 'view' => $this, + ]) ?> +
+
+ +
-endContent(); ?> \ No newline at end of file +endContent(); ?> diff --git a/extensions/apidoc/templates/bootstrap/layouts/main.php b/extensions/apidoc/templates/bootstrap/layouts/main.php index 602e546b98a..3a645581ac5 100644 --- a/extensions/apidoc/templates/bootstrap/layouts/main.php +++ b/extensions/apidoc/templates/bootstrap/layouts/main.php @@ -14,12 +14,12 @@ // Navbar hides initial content when jumping to in-page anchor // https://github.com/twbs/bootstrap/issues/1768 $this->registerJs(<<beginPage(); @@ -27,67 +27,66 @@ - - - - head() ?> - <?= Html::encode($this->context->pageTitle) ?> + + + + head() ?> + <?= Html::encode($this->context->pageTitle) ?> beginBody() ?>
- $this->context->pageTitle, - 'brandUrl' => ($this->context->apiUrl === null && $this->context->guideUrl !== null) ? './guide-index.html' : './index.html', - 'options' => [ - 'class' => 'navbar-inverse navbar-fixed-top', - ], - 'renderInnerContainer' => false, - 'view' => $this, - ]); - $nav = []; + $this->context->pageTitle, + 'brandUrl' => ($this->context->apiUrl === null && $this->context->guideUrl !== null) ? './guide-index.html' : './index.html', + 'options' => [ + 'class' => 'navbar-inverse navbar-fixed-top', + ], + 'renderInnerContainer' => false, + 'view' => $this, + ]); + $nav = []; - if ($this->context->apiUrl !== null) { - $nav[] = ['label' => 'Class reference', 'url' => rtrim($this->context->apiUrl, '/') . '/index.html']; - if (!empty($this->context->extensions)) - { - $extItems = []; - foreach ($this->context->extensions as $ext) { - $extItems[] = [ - 'label' => $ext, - 'url' => "./ext-{$ext}-index.html", - ]; - } - $nav[] = ['label' => 'Extensions', 'items' => $extItems]; - } - } + if ($this->context->apiUrl !== null) { + $nav[] = ['label' => 'Class reference', 'url' => rtrim($this->context->apiUrl, '/') . '/index.html']; + if (!empty($this->context->extensions)) { + $extItems = []; + foreach ($this->context->extensions as $ext) { + $extItems[] = [ + 'label' => $ext, + 'url' => "./ext-{$ext}-index.html", + ]; + } + $nav[] = ['label' => 'Extensions', 'items' => $extItems]; + } + } - if ($this->context->guideUrl !== null) { - $nav[] = ['label' => 'Guide', 'url' => rtrim($this->context->guideUrl, '/') . '/' . BaseRenderer::GUIDE_PREFIX . 'index.html']; - } + if ($this->context->guideUrl !== null) { + $nav[] = ['label' => 'Guide', 'url' => rtrim($this->context->guideUrl, '/') . '/' . BaseRenderer::GUIDE_PREFIX . 'index.html']; + } - echo Nav::widget([ - 'options' => ['class' => 'navbar-nav'], - 'items' => $nav, - 'view' => $this, - 'params' => [], - ]); - NavBar::end(); - ?> + echo Nav::widget([ + 'options' => ['class' => 'navbar-nav'], + 'items' => $nav, + 'view' => $this, + 'params' => [], + ]); + NavBar::end(); + ?> - +
- © My Company

*/ ?> -

Page generated on

- + © My Company

*/ ?> +

Page generated on

+
endBody() ?> -endPage() ?> \ No newline at end of file +endPage() ?> diff --git a/extensions/apidoc/templates/bootstrap/views/index.php b/extensions/apidoc/templates/bootstrap/views/index.php index ce866e8fab5..a665c3f120f 100644 --- a/extensions/apidoc/templates/bootstrap/views/index.php +++ b/extensions/apidoc/templates/bootstrap/views/index.php @@ -13,28 +13,28 @@ $renderer = $this->context; if (isset($readme)) { - echo \yii\apidoc\helpers\ApiMarkdown::process($readme); + echo \yii\apidoc\helpers\ApiMarkdown::process($readme); } ?>

Class Reference

- - - - - - - - - + + + + + + + + + $class): ?> - - - - + + + +
ClassDescription
ClassDescription
createTypeLink($class, $class, $class->name) ?>shortDescription, $class, true) ?>
createTypeLink($class, $class, $class->name) ?>shortDescription, $class, true) ?>
diff --git a/extensions/apidoc/templates/html/ApiRenderer.php b/extensions/apidoc/templates/html/ApiRenderer.php index 06dcab58734..d8d8c7fda06 100644 --- a/extensions/apidoc/templates/html/ApiRenderer.php +++ b/extensions/apidoc/templates/html/ApiRenderer.php @@ -30,257 +30,265 @@ */ class ApiRenderer extends BaseApiRenderer implements ViewContextInterface { - /** - * @var string string to use as the title of the generated page. - */ - public $pageTitle; - /** - * @var string path or alias of the layout file to use. - */ - public $layout; - /** - * @var string path or alias of the view file to use for rendering types (classes, interfaces, traits). - */ - public $typeView = '@yii/apidoc/templates/html/views/type.php'; - /** - * @var string path or alias of the view file to use for rendering the index page. - */ - public $indexView = '@yii/apidoc/templates/html/views/index.php'; - /** - * @var View - */ - private $_view; - private $_targetDir; - - - public function init() - { - parent::init(); - - if ($this->pageTitle === null) { - $this->pageTitle = 'Yii Framework 2.0 API Documentation'; // TODO guess page title - } - } - - /** - * @return View the view instance - */ - public function getView() - { - if ($this->_view === null) { - $this->_view = new View(); - $assetPath = Yii::getAlias($this->_targetDir) . '/assets'; - if (!is_dir($assetPath)) { - mkdir($assetPath); - } - $this->_view->assetManager = new AssetManager([ - 'basePath' => $assetPath, - 'baseUrl' => './assets', - ]); - } - return $this->_view; - } - - /** - * Renders a given [[Context]]. - * - * @param Context $context the api documentation context to render. - * @param $targetDir - */ - public function render($context, $targetDir) - { - $this->apiContext = $context; - $this->_targetDir = $targetDir; - - $types = array_merge($context->classes, $context->interfaces, $context->traits); - $typeCount = count($types) + 1; - - if ($this->controller !== null) { - Console::startProgress(0, $typeCount, 'Rendering files: ', false); - } - $done = 0; - foreach ($types as $type) { - $fileContent = $this->renderWithLayout($this->typeView, [ - 'type' => $type, - 'apiContext' => $context, - 'types' => $types, - ]); - file_put_contents($targetDir . '/' . $this->generateFileName($type->name), $fileContent); - - if ($this->controller !== null) { - Console::updateProgress(++$done, $typeCount); - } - } - - $indexFileContent = $this->renderWithLayout($this->indexView, [ - 'apiContext' => $context, - 'types' => $types, - ]); - file_put_contents($targetDir . '/index.html', $indexFileContent); - - if ($this->controller !== null) { - Console::updateProgress(++$done, $typeCount); - Console::endProgress(true); - $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); - } - } - - protected function renderWithLayout($viewFile, $params) - { - $output = $this->getView()->render($viewFile, $params, $this); - if ($this->layout !== false) { - $params['content'] = $output; - return $this->getView()->renderFile($this->layout, $params, $this); - } else { - return $output; - } - } - - /** - * @param ClassDoc $class - * @return string - */ - public function renderInheritance($class) - { - $parents = []; - $parents[] = $this->createTypeLink($class); - while ($class->parentClass !== null) { - if (isset($this->apiContext->classes[$class->parentClass])) { - $class = $this->apiContext->classes[$class->parentClass]; - $parents[] = $this->createTypeLink($class); - } else { - $parents[] = $this->createTypeLink($class->parentClass); - break; - } - } - return implode(" »\n", $parents); - } - - /** - * @param array $names - * @return string - */ - public function renderInterfaces($names) - { - $interfaces = []; - sort($names, SORT_STRING); - foreach ($names as $interface) { - if (isset($this->apiContext->interfaces[$interface])) { - $interfaces[] = $this->createTypeLink($this->apiContext->interfaces[$interface]); - } else { - $interfaces[] = $this->createTypeLink($interface); - } - } - return implode(', ', $interfaces); - } - - /** - * @param array $names - * @return string - */ - public function renderTraits($names) - { - $traits = []; - sort($names, SORT_STRING); - foreach ($names as $trait) { - if (isset($this->apiContext->traits[$trait])) { - $traits[] = $this->createTypeLink($this->apiContext->traits[$trait]); - } else { - $traits[] = $this->createTypeLink($trait); - } - } - return implode(', ', $traits); - } - - /** - * @param array $names - * @return string - */ - public function renderClasses($names) - { - $classes = []; - sort($names, SORT_STRING); - foreach ($names as $class) { - if (isset($this->apiContext->classes[$class])) { - $classes[] = $this->createTypeLink($this->apiContext->classes[$class]); - } else { - $classes[] = $this->createTypeLink($class); - } - } - return implode(', ', $classes); - } - - /** - * @param PropertyDoc $property - * @return string - */ - public function renderPropertySignature($property) - { - if ($property->getter !== null || $property->setter !== null) { - $sig = []; - if ($property->getter !== null) { - $sig[] = $this->renderMethodSignature($property->getter); - } - if ($property->setter !== null) { - $sig[] = $this->renderMethodSignature($property->setter); - } - return implode('
', $sig); - } - return $this->createTypeLink($property->types) . ' ' . $this->createSubjectLink($property, $property->name) . ' ' - . ApiMarkdown::highlight('= ' . ($property->defaultValue === null ? 'null' : $property->defaultValue), 'php'); - } - - /** - * @param MethodDoc $method - * @return string - */ - public function renderMethodSignature($method) - { - $params = []; - foreach ($method->params as $param) { - $params[] = (empty($param->typeHint) ? '' : $param->typeHint . ' ') - . ($param->isPassedByReference ? '&' : '') - . $param->name - . ($param->isOptional ? ' = ' . $param->defaultValue : ''); - } - - return ($method->isReturnByReference ? '&' : '') - . ($method->returnType === null ? 'void' : $this->createTypeLink($method->returnTypes)) - . ' ' . $this->createSubjectLink($method, $method->name) . '' - . ApiMarkdown::highlight(str_replace(' ', ' ', '( ' . implode(', ', $params) . ' )'), 'php'); - } - - public function generateApiUrl($typeName) - { - return $this->generateFileName($typeName); - } - - protected function generateFileName($typeName) - { - return strtolower(str_replace('\\', '-', $typeName)) . '.html'; - } - - /** - * Finds the view file corresponding to the specified relative view name. - * @param string $view a relative view name. The name does NOT start with a slash. - * @return string the view file path. Note that the file may not exist. - */ - public function findViewFile($view) - { - return Yii::getAlias('@yii/apidoc/templates/html/views/' . $view); - } - - /** - * @inheritdoc - */ - protected function generateLink($text, $href, $options = []) - { - $options['href'] = $href; - return Html::a($text, null, $options); - } - - public function getSourceUrl($type) - { - return null; - } + /** + * @var string string to use as the title of the generated page. + */ + public $pageTitle; + /** + * @var string path or alias of the layout file to use. + */ + public $layout; + /** + * @var string path or alias of the view file to use for rendering types (classes, interfaces, traits). + */ + public $typeView = '@yii/apidoc/templates/html/views/type.php'; + /** + * @var string path or alias of the view file to use for rendering the index page. + */ + public $indexView = '@yii/apidoc/templates/html/views/index.php'; + /** + * @var View + */ + private $_view; + private $_targetDir; + + public function init() + { + parent::init(); + + if ($this->pageTitle === null) { + $this->pageTitle = 'Yii Framework 2.0 API Documentation'; // TODO guess page title + } + } + + /** + * @return View the view instance + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = new View(); + $assetPath = Yii::getAlias($this->_targetDir) . '/assets'; + if (!is_dir($assetPath)) { + mkdir($assetPath); + } + $this->_view->assetManager = new AssetManager([ + 'basePath' => $assetPath, + 'baseUrl' => './assets', + ]); + } + + return $this->_view; + } + + /** + * Renders a given [[Context]]. + * + * @param Context $context the api documentation context to render. + * @param $targetDir + */ + public function render($context, $targetDir) + { + $this->apiContext = $context; + $this->_targetDir = $targetDir; + + $types = array_merge($context->classes, $context->interfaces, $context->traits); + $typeCount = count($types) + 1; + + if ($this->controller !== null) { + Console::startProgress(0, $typeCount, 'Rendering files: ', false); + } + $done = 0; + foreach ($types as $type) { + $fileContent = $this->renderWithLayout($this->typeView, [ + 'type' => $type, + 'apiContext' => $context, + 'types' => $types, + ]); + file_put_contents($targetDir . '/' . $this->generateFileName($type->name), $fileContent); + + if ($this->controller !== null) { + Console::updateProgress(++$done, $typeCount); + } + } + + $indexFileContent = $this->renderWithLayout($this->indexView, [ + 'apiContext' => $context, + 'types' => $types, + ]); + file_put_contents($targetDir . '/index.html', $indexFileContent); + + if ($this->controller !== null) { + Console::updateProgress(++$done, $typeCount); + Console::endProgress(true); + $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); + } + } + + protected function renderWithLayout($viewFile, $params) + { + $output = $this->getView()->render($viewFile, $params, $this); + if ($this->layout !== false) { + $params['content'] = $output; + + return $this->getView()->renderFile($this->layout, $params, $this); + } else { + return $output; + } + } + + /** + * @param ClassDoc $class + * @return string + */ + public function renderInheritance($class) + { + $parents = []; + $parents[] = $this->createTypeLink($class); + while ($class->parentClass !== null) { + if (isset($this->apiContext->classes[$class->parentClass])) { + $class = $this->apiContext->classes[$class->parentClass]; + $parents[] = $this->createTypeLink($class); + } else { + $parents[] = $this->createTypeLink($class->parentClass); + break; + } + } + + return implode(" »\n", $parents); + } + + /** + * @param array $names + * @return string + */ + public function renderInterfaces($names) + { + $interfaces = []; + sort($names, SORT_STRING); + foreach ($names as $interface) { + if (isset($this->apiContext->interfaces[$interface])) { + $interfaces[] = $this->createTypeLink($this->apiContext->interfaces[$interface]); + } else { + $interfaces[] = $this->createTypeLink($interface); + } + } + + return implode(', ', $interfaces); + } + + /** + * @param array $names + * @return string + */ + public function renderTraits($names) + { + $traits = []; + sort($names, SORT_STRING); + foreach ($names as $trait) { + if (isset($this->apiContext->traits[$trait])) { + $traits[] = $this->createTypeLink($this->apiContext->traits[$trait]); + } else { + $traits[] = $this->createTypeLink($trait); + } + } + + return implode(', ', $traits); + } + + /** + * @param array $names + * @return string + */ + public function renderClasses($names) + { + $classes = []; + sort($names, SORT_STRING); + foreach ($names as $class) { + if (isset($this->apiContext->classes[$class])) { + $classes[] = $this->createTypeLink($this->apiContext->classes[$class]); + } else { + $classes[] = $this->createTypeLink($class); + } + } + + return implode(', ', $classes); + } + + /** + * @param PropertyDoc $property + * @return string + */ + public function renderPropertySignature($property) + { + if ($property->getter !== null || $property->setter !== null) { + $sig = []; + if ($property->getter !== null) { + $sig[] = $this->renderMethodSignature($property->getter); + } + if ($property->setter !== null) { + $sig[] = $this->renderMethodSignature($property->setter); + } + + return implode('
', $sig); + } + + return $this->createTypeLink($property->types) . ' ' . $this->createSubjectLink($property, $property->name) . ' ' + . ApiMarkdown::highlight('= ' . ($property->defaultValue === null ? 'null' : $property->defaultValue), 'php'); + } + + /** + * @param MethodDoc $method + * @return string + */ + public function renderMethodSignature($method) + { + $params = []; + foreach ($method->params as $param) { + $params[] = (empty($param->typeHint) ? '' : $param->typeHint . ' ') + . ($param->isPassedByReference ? '&' : '') + . $param->name + . ($param->isOptional ? ' = ' . $param->defaultValue : ''); + } + + return ($method->isReturnByReference ? '&' : '') + . ($method->returnType === null ? 'void' : $this->createTypeLink($method->returnTypes)) + . ' ' . $this->createSubjectLink($method, $method->name) . '' + . ApiMarkdown::highlight(str_replace(' ', ' ', '( ' . implode(', ', $params) . ' )'), 'php'); + } + + public function generateApiUrl($typeName) + { + return $this->generateFileName($typeName); + } + + protected function generateFileName($typeName) + { + return strtolower(str_replace('\\', '-', $typeName)) . '.html'; + } + + /** + * Finds the view file corresponding to the specified relative view name. + * @param string $view a relative view name. The name does NOT start with a slash. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view) + { + return Yii::getAlias('@yii/apidoc/templates/html/views/' . $view); + } + + /** + * @inheritdoc + */ + protected function generateLink($text, $href, $options = []) + { + $options['href'] = $href; + + return Html::a($text, null, $options); + } + + public function getSourceUrl($type) + { + return null; + } } diff --git a/extensions/apidoc/templates/html/GuideRenderer.php b/extensions/apidoc/templates/html/GuideRenderer.php index e6dd4c9889f..d28564ba8a5 100644 --- a/extensions/apidoc/templates/html/GuideRenderer.php +++ b/extensions/apidoc/templates/html/GuideRenderer.php @@ -25,136 +25,137 @@ */ abstract class GuideRenderer extends BaseGuideRenderer { - public $pageTitle; - public $layout; - - /** - * @var View - */ - private $_view; - private $_targetDir; - - - public function init() - { - parent::init(); - - if ($this->pageTitle === null) { - $this->pageTitle = 'Yii Framework 2.0 API Documentation'; // TODO guess page title - } - } - - /** - * @return View the view instance - */ - public function getView() - { - if ($this->_view === null) { - $this->_view = new View(); - $assetPath = Yii::getAlias($this->_targetDir) . '/assets'; - if (!is_dir($assetPath)) { - mkdir($assetPath); - } - $this->_view->assetManager = new AssetManager([ - 'basePath' => $assetPath, - 'baseUrl' => './assets', - ]); - } - return $this->_view; - } - - - /** - * Renders a given [[Context]]. - * - * @param Controller $controller the apidoc controller instance. Can be used to control output. - */ - public function render($files, $targetDir) - { - $this->_targetDir = $targetDir; - - $fileCount = count($files) + 1; - if ($this->controller !== null) { - Console::startProgress(0, $fileCount, 'Rendering markdown files: ', false); - } - $done = 0; - $fileData = []; - $headlines = []; - foreach ($files as $file) { - $fileData[$file] = file_get_contents($file); - if (basename($file) == 'index.md') { - continue; // to not add index file to nav - } - if (preg_match("/^(.*)\n=+/", $fileData[$file], $matches)) { - $headlines[$file] = $matches[1]; - } else { - $headlines[$file] = basename($file); - } - } - - foreach ($fileData as $file => $content) { - $output = ApiMarkdown::process($content); // TODO generate links to yiiframework.com by default - $output = $this->fixMarkdownLinks($output); - if ($this->layout !== false) { - $params = [ - 'headlines' => $headlines, - 'currentFile' => $file, - 'content' => $output, - ]; - $output = $this->getView()->renderFile($this->layout, $params, $this); - } - $fileName = $this->generateGuideFileName($file); - file_put_contents($targetDir . '/' . $fileName, $output); - - if ($this->controller !== null) { - Console::updateProgress(++$done, $fileCount); - } - } - if ($this->controller !== null) { - Console::updateProgress(++$done, $fileCount); - Console::endProgress(true); - $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); - } - } - - protected function generateGuideFileName($file) - { - return static::GUIDE_PREFIX . basename($file, '.md') . '.html'; - } - - public function getGuideReferences() - { - // TODO implement for api docs + public $pageTitle; + public $layout; + + /** + * @var View + */ + private $_view; + private $_targetDir; + + public function init() + { + parent::init(); + + if ($this->pageTitle === null) { + $this->pageTitle = 'Yii Framework 2.0 API Documentation'; // TODO guess page title + } + } + + /** + * @return View the view instance + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = new View(); + $assetPath = Yii::getAlias($this->_targetDir) . '/assets'; + if (!is_dir($assetPath)) { + mkdir($assetPath); + } + $this->_view->assetManager = new AssetManager([ + 'basePath' => $assetPath, + 'baseUrl' => './assets', + ]); + } + + return $this->_view; + } + + /** + * Renders a given [[Context]]. + * + * @param Controller $controller the apidoc controller instance. Can be used to control output. + */ + public function render($files, $targetDir) + { + $this->_targetDir = $targetDir; + + $fileCount = count($files) + 1; + if ($this->controller !== null) { + Console::startProgress(0, $fileCount, 'Rendering markdown files: ', false); + } + $done = 0; + $fileData = []; + $headlines = []; + foreach ($files as $file) { + $fileData[$file] = file_get_contents($file); + if (basename($file) == 'index.md') { + continue; // to not add index file to nav + } + if (preg_match("/^(.*)\n=+/", $fileData[$file], $matches)) { + $headlines[$file] = $matches[1]; + } else { + $headlines[$file] = basename($file); + } + } + + foreach ($fileData as $file => $content) { + $output = ApiMarkdown::process($content); // TODO generate links to yiiframework.com by default + $output = $this->fixMarkdownLinks($output); + if ($this->layout !== false) { + $params = [ + 'headlines' => $headlines, + 'currentFile' => $file, + 'content' => $output, + ]; + $output = $this->getView()->renderFile($this->layout, $params, $this); + } + $fileName = $this->generateGuideFileName($file); + file_put_contents($targetDir . '/' . $fileName, $output); + + if ($this->controller !== null) { + Console::updateProgress(++$done, $fileCount); + } + } + if ($this->controller !== null) { + Console::updateProgress(++$done, $fileCount); + Console::endProgress(true); + $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); + } + } + + protected function generateGuideFileName($file) + { + return static::GUIDE_PREFIX . basename($file, '.md') . '.html'; + } + + public function getGuideReferences() + { + // TODO implement for api docs // $refs = []; -// foreach($this->markDownFiles as $file) { +// foreach ($this->markDownFiles as $file) { // $refName = 'guide-' . basename($file, '.md'); // $refs[$refName] = ['url' => $this->generateGuideFileName($file)]; // } // return $refs; - } - - protected function fixMarkdownLinks($content) - { - $content = preg_replace('/href\s*=\s*"([^"\/]+)\.md(#.*)?"/i', 'href="' . static::GUIDE_PREFIX . '\1.html\2"', $content); - return $content; - } - - /** - * @inheritdoc - */ - protected function generateLink($text, $href, $options = []) - { - $options['href'] = $href; - return Html::a($text, null, $options); - } - - /** - * Generate an url to a type in apidocs - * @param $typeName - * @return mixed - */ - public function generateApiUrl($typeName) - { - return rtrim($this->apiUrl, '/') . '/' . strtolower(str_replace('\\', '-', $typeName)) . '.html'; - } + } + + protected function fixMarkdownLinks($content) + { + $content = preg_replace('/href\s*=\s*"([^"\/]+)\.md(#.*)?"/i', 'href="' . static::GUIDE_PREFIX . '\1.html\2"', $content); + + return $content; + } + + /** + * @inheritdoc + */ + protected function generateLink($text, $href, $options = []) + { + $options['href'] = $href; + + return Html::a($text, null, $options); + } + + /** + * Generate an url to a type in apidocs + * @param $typeName + * @return mixed + */ + public function generateApiUrl($typeName) + { + return rtrim($this->apiUrl, '/') . '/' . strtolower(str_replace('\\', '-', $typeName)) . '.html'; + } } diff --git a/extensions/apidoc/templates/html/views/constSummary.php b/extensions/apidoc/templates/html/views/constSummary.php index a85df814719..6d1fbee1a39 100644 --- a/extensions/apidoc/templates/html/views/constSummary.php +++ b/extensions/apidoc/templates/html/views/constSummary.php @@ -13,33 +13,33 @@ $renderer = $this->context; if (empty($type->constants)) { - return; + return; } $constants = $type->constants; ArrayHelper::multisort($constants, 'name'); ?>
-

Constants

+

Constants

-

Hide inherited constants

+

Hide inherited constants

- - - - - - - - - - - - definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>"> - - - - - - -
ConstantValueDescriptionDefined By
name ?>value ?>shortDescription . "\n" . $constant->description, $constant->definedBy, true) ?>createTypeLink($constant->definedBy) ?>
-
\ No newline at end of file + + + + + + + + + + + + definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>"> + + + + + + +
ConstantValueDescriptionDefined By
name ?>value ?>shortDescription . "\n" . $constant->description, $constant->definedBy, true) ?>createTypeLink($constant->definedBy) ?>
+ diff --git a/extensions/apidoc/templates/html/views/eventDetails.php b/extensions/apidoc/templates/html/views/eventDetails.php index f3ecb9ae879..44a756265af 100644 --- a/extensions/apidoc/templates/html/views/eventDetails.php +++ b/extensions/apidoc/templates/html/views/eventDetails.php @@ -14,7 +14,7 @@ $events = $type->getNativeEvents(); if (empty($events)) { - return; + return; } ArrayHelper::multisort($events, 'name'); ?> @@ -22,33 +22,32 @@
-
- - createSubjectLink($event, '', [ - 'title' => 'direct link to this method', - 'class' => 'tool-link hash', - ]) ?> - - getSourceUrl($event->definedBy, $event->startLine)) !== null): ?> - - - - - - name ?> - - event - since)): ?> - (available since version since ?>) - - -
- -
- description, $type) ?> - - render('seeAlso', ['object' => $event]) ?> -
+
+ + createSubjectLink($event, '', [ + 'title' => 'direct link to this method', + 'class' => 'tool-link hash', + ]) ?> + + getSourceUrl($event->definedBy, $event->startLine)) !== null): ?> + + + + + name ?> + + event + since)): ?> + (available since version since ?>) + + +
+ +
+ description, $type) ?> + + render('seeAlso', ['object' => $event]) ?> +
diff --git a/extensions/apidoc/templates/html/views/eventSummary.php b/extensions/apidoc/templates/html/views/eventSummary.php index ce8c0ab3edc..33fe21ee31e 100644 --- a/extensions/apidoc/templates/html/views/eventSummary.php +++ b/extensions/apidoc/templates/html/views/eventSummary.php @@ -13,38 +13,38 @@ $renderer = $this->context; if (empty($type->events)) { - return; + return; } $events = $type->events; ArrayHelper::multisort($events, 'name'); ?>
-

Events

+

Events

-

Hide inherited events

+

Hide inherited events

- - - - - - - - - - - - definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>"> - - - - - - -
EventTypeDescriptionDefined By
createSubjectLink($event) ?>createTypeLink($event->types) ?> - shortDescription, $event->definedBy, true) ?> - since)): ?> - (available since version since ?>) - - createTypeLink($event->definedBy) ?>
+ + + + + + + + + + + + definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>"> + + + + + + +
EventTypeDescriptionDefined By
createSubjectLink($event) ?>createTypeLink($event->types) ?> + shortDescription, $event->definedBy, true) ?> + since)): ?> + (available since version since ?>) + + createTypeLink($event->definedBy) ?>
diff --git a/extensions/apidoc/templates/html/views/methodDetails.php b/extensions/apidoc/templates/html/views/methodDetails.php index a3db465ebf4..88d9062d3b9 100644 --- a/extensions/apidoc/templates/html/views/methodDetails.php +++ b/extensions/apidoc/templates/html/views/methodDetails.php @@ -15,7 +15,7 @@ $methods = $type->getNativeMethods(); if (empty($methods)) { - return; + return; } ArrayHelper::multisort($methods, 'name'); ?> @@ -24,62 +24,62 @@
-
- - createSubjectLink($method, '', [ - 'title' => 'direct link to this method', - 'class' => 'tool-link hash', - ]) ?> +
+ + createSubjectLink($method, '', [ + 'title' => 'direct link to this method', + 'class' => 'tool-link hash', + ]) ?> - getSourceUrl($method->definedBy, $method->startLine)) !== null): ?> - - - + getSourceUrl($method->definedBy, $method->startLine)) !== null): ?> + + + - name ?>() - - visibility ?> - method - since)): ?> - (available since version since ?>) - - -
+ name ?>() + + visibility ?> + method + since)): ?> + (available since version since ?>) + + +
-
-

shortDescription, $type, true) ?>

+
+

shortDescription, $type, true) ?>

- description, $type) ?> + description, $type) ?> - render('seeAlso', ['object' => $method]) ?> -
+ render('seeAlso', ['object' => $method]) ?> +
- - - params) || !empty($method->return) || !empty($method->exceptions)): ?> - params as $param): ?> - - - - - - - return)): ?> - - - - - - - exceptions as $exception => $description): ?> - - - - - - - -
renderMethodSignature($method) ?>
name, 'php') ?>createTypeLink($param->types) ?>description, $type) ?>
returncreateTypeLink($method->returnTypes) ?>return, $type) ?>
throwscreateTypeLink($exception) ?>
+ + + params) || !empty($method->return) || !empty($method->exceptions)): ?> + params as $param): ?> + + + + + + + return)): ?> + + + + + + + exceptions as $exception => $description): ?> + + + + + + + +
renderMethodSignature($method) ?>
name, 'php') ?>createTypeLink($param->types) ?>description, $type) ?>
returncreateTypeLink($method->returnTypes) ?>return, $type) ?>
throwscreateTypeLink($exception) ?>
renderPartial('sourceCode',array('object'=>$method)); ?> diff --git a/extensions/apidoc/templates/html/views/methodSummary.php b/extensions/apidoc/templates/html/views/methodSummary.php index 9d264aa63c1..a0b0903c759 100644 --- a/extensions/apidoc/templates/html/views/methodSummary.php +++ b/extensions/apidoc/templates/html/views/methodSummary.php @@ -16,7 +16,7 @@ $renderer = $this->context; if ($protected && count($type->getProtectedMethods()) == 0 || !$protected && count($type->getPublicMethods()) == 0) { - return; + return; } ?>
@@ -26,9 +26,9 @@ - - - + + + @@ -37,13 +37,13 @@ $methods = $type->methods; ArrayHelper::multisort($methods, 'name'); foreach ($methods as $method): ?> - visibility == 'protected' || !$protected && $method->visibility != 'protected'): ?> - definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>()"> - - - - - + visibility == 'protected' || !$protected && $method->visibility != 'protected'): ?> + definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>()"> + + + + +
MethodDescriptionDefined BycreateSubjectLink($method, $method->name.'()') ?>shortDescription, $method->definedBy, true) ?>createTypeLink($method->definedBy, $type) ?>
createSubjectLink($method, $method->name.'()') ?>shortDescription, $method->definedBy, true) ?>createTypeLink($method->definedBy, $type) ?>
-
\ No newline at end of file +
diff --git a/extensions/apidoc/templates/html/views/propertyDetails.php b/extensions/apidoc/templates/html/views/propertyDetails.php index 17aa86475dc..00dee683c46 100644 --- a/extensions/apidoc/templates/html/views/propertyDetails.php +++ b/extensions/apidoc/templates/html/views/propertyDetails.php @@ -15,7 +15,7 @@ $properties = $type->getNativeProperties(); if (empty($properties)) { - return; + return; } ArrayHelper::multisort($properties, 'name'); ?> @@ -24,36 +24,35 @@
-
- - createSubjectLink($property, '', [ - 'title' => 'direct link to this method', - 'class' => 'tool-link hash', - ]) ?> - - getSourceUrl($property->definedBy, $property->startLine)) !== null): ?> - - - - - - name ?> - - visibility ?> - getIsReadOnly()) echo ' read-only '; ?> - getIsWriteOnly()) echo ' write-only '; ?> - property - since)): ?> - (available since version since ?>) - - -
- -
renderPropertySignature($property); ?>
- - description, $type) ?> - - render('seeAlso', ['object' => $property]) ?> +
+ + createSubjectLink($property, '', [ + 'title' => 'direct link to this method', + 'class' => 'tool-link hash', + ]) ?> + + getSourceUrl($property->definedBy, $property->startLine)) !== null): ?> + + + + + name ?> + + visibility ?> + getIsReadOnly()) echo ' read-only '; ?> + getIsWriteOnly()) echo ' write-only '; ?> + property + since)): ?> + (available since version since ?>) + + +
+ +
renderPropertySignature($property); ?>
+ + description, $type) ?> + + render('seeAlso', ['object' => $property]) ?>
diff --git a/extensions/apidoc/templates/html/views/propertySummary.php b/extensions/apidoc/templates/html/views/propertySummary.php index 250c7eadaa3..7e3c1c81823 100644 --- a/extensions/apidoc/templates/html/views/propertySummary.php +++ b/extensions/apidoc/templates/html/views/propertySummary.php @@ -15,7 +15,7 @@ $renderer = $this->context; if ($protected && count($type->getProtectedProperties()) == 0 || !$protected && count($type->getPublicProperties()) == 0) { - return; + return; } ?>
@@ -25,10 +25,10 @@ - - - - + + + + @@ -37,14 +37,14 @@ $properties = $type->properties; ArrayHelper::multisort($properties, 'name'); foreach ($properties as $property): ?> - visibility == 'protected' || !$protected && $property->visibility != 'protected'): ?> - definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>"> - - - - - - + visibility == 'protected' || !$protected && $property->visibility != 'protected'): ?> + definedBy != $type->name ? ' class="inherited"' : '' ?> id="name ?>"> + + + + + +
PropertyTypeDescriptionDefined BycreateSubjectLink($property) ?>createTypeLink($property->types) ?>shortDescription, $property->definedBy, true) ?>createTypeLink($property->definedBy) ?>
createSubjectLink($property) ?>createTypeLink($property->types) ?>shortDescription, $property->definedBy, true) ?>createTypeLink($property->definedBy) ?>
-
\ No newline at end of file + diff --git a/extensions/apidoc/templates/html/views/seeAlso.php b/extensions/apidoc/templates/html/views/seeAlso.php index 4fcbef9ad47..e244c2ef07b 100644 --- a/extensions/apidoc/templates/html/views/seeAlso.php +++ b/extensions/apidoc/templates/html/views/seeAlso.php @@ -7,26 +7,26 @@ $see = []; foreach ($object->tags as $tag) { - /** @var $tag phpDocumentor\Reflection\DocBlock\Tag\SeeTag */ - if (get_class($tag) == 'phpDocumentor\Reflection\DocBlock\Tag\SeeTag') { - $ref = $tag->getReference(); - if (strpos($ref, '://') === false) { - $ref = '[[' . $ref . ']]'; - } - $see[] = rtrim(\yii\apidoc\helpers\ApiMarkdown::process($ref . ' ' . $tag->getDescription(), $object->definedBy, true), ". \r\n"); - } + /** @var $tag phpDocumentor\Reflection\DocBlock\Tag\SeeTag */ + if (get_class($tag) == 'phpDocumentor\Reflection\DocBlock\Tag\SeeTag') { + $ref = $tag->getReference(); + if (strpos($ref, '://') === false) { + $ref = '[[' . $ref . ']]'; + } + $see[] = rtrim(\yii\apidoc\helpers\ApiMarkdown::process($ref . ' ' . $tag->getDescription(), $object->definedBy, true), ". \r\n"); + } } if (empty($see)) { - return; + return; } elseif (count($see) == 1) { - echo '

See also ' . reset($see) . '.

'; + echo '

See also ' . reset($see) . '.

'; } else { - echo '

See also:

'; + echo '

See also:

'; } diff --git a/extensions/apidoc/templates/html/views/type.php b/extensions/apidoc/templates/html/views/type.php index aed3018b9ce..74954a73d53 100644 --- a/extensions/apidoc/templates/html/views/type.php +++ b/extensions/apidoc/templates/html/views/type.php @@ -14,74 +14,74 @@ $renderer = $this->context; ?>

isFinal) { - echo 'Final '; - } - if ($type->isAbstract) { - echo 'Abstract '; - } - echo 'Class '; - } - echo $type->name; + if ($type instanceof InterfaceDoc) { + echo 'Interface '; + } elseif ($type instanceof TraitDoc) { + echo 'Trait '; + } else { + if ($type->isFinal) { + echo 'Final '; + } + if ($type->isAbstract) { + echo 'Abstract '; + } + echo 'Class '; + } + echo $type->name; ?>

- - - - - - - - interfaces)): ?> - - - traits)): ?> - - - subclasses)): ?> - - - implementedBy)): ?> - - - usedBy)): ?> - - - since)): ?> - - - getSourceUrl($type)) !== null): ?> - - - - - + + + + + + + + interfaces)): ?> + + + traits)): ?> + + + subclasses)): ?> + + + implementedBy)): ?> + + + usedBy)): ?> + + + since)): ?> + + + getSourceUrl($type)) !== null): ?> + + + + +
InheritancerenderInheritance($type) ?>
ImplementsrenderInterfaces($type->interfaces) ?>
Uses TraitsrenderTraits($type->traits) ?>
SubclassesrenderClasses($type->subclasses) ?>
Implemented byrenderClasses($type->implementedBy) ?>
Implemented byrenderClasses($type->usedBy) ?>
Available since versionsince ?>
Source Code
InheritancerenderInheritance($type) ?>
ImplementsrenderInterfaces($type->interfaces) ?>
Uses TraitsrenderTraits($type->traits) ?>
SubclassesrenderClasses($type->subclasses) ?>
Implemented byrenderClasses($type->implementedBy) ?>
Implemented byrenderClasses($type->usedBy) ?>
Available since versionsince ?>
Source Code
- shortDescription, $type, true) ?> -

description, $type) ?>

+ shortDescription, $type, true) ?> +

description, $type) ?>

@@ -101,5 +101,5 @@ render('@yii/apidoc/templates/html/views/propertyDetails', ['type' => $type]) ?> render('@yii/apidoc/templates/html/views/methodDetails', ['type' => $type]) ?> - render('@yii/apidoc/templates/html/views/eventDetails', ['type' => $type]) ?> + render('@yii/apidoc/templates/html/views/eventDetails', ['type' => $type]) ?> diff --git a/extensions/apidoc/templates/online/ApiRenderer.php b/extensions/apidoc/templates/online/ApiRenderer.php index d0dea029e97..664162beed8 100644 --- a/extensions/apidoc/templates/online/ApiRenderer.php +++ b/extensions/apidoc/templates/online/ApiRenderer.php @@ -20,49 +20,49 @@ */ class ApiRenderer extends \yii\apidoc\templates\html\ApiRenderer { - public $layout = false; - public $indexView = '@yii/apidoc/templates/online/views/index.php'; + public $layout = false; + public $indexView = '@yii/apidoc/templates/online/views/index.php'; - public $pageTitle = 'Yii Framework 2.0 API Documentation'; + public $pageTitle = 'Yii Framework 2.0 API Documentation'; - /** - * @inheritdoc - */ - public function render($context, $targetDir) - { - parent::render($context, $targetDir); + /** + * @inheritdoc + */ + public function render($context, $targetDir) + { + parent::render($context, $targetDir); - if ($this->controller !== null) { - $this->controller->stdout("writing packages file..."); - } - $packages = []; - $notNamespaced = []; - foreach (array_merge($context->classes, $context->interfaces, $context->traits) as $type) { - /** @var TypeDoc $type */ - if (empty($type->namespace)) { - $notNamespaced[] = str_replace('\\', '-', $type->name); - } else { - $packages[$type->namespace][] = str_replace('\\', '-', $type->name); - } - } - ksort($packages); - $packages = array_merge(['Not namespaced' => $notNamespaced], $packages); - foreach ($packages as $name => $classes) { - sort($packages[$name]); - } - file_put_contents($targetDir . '/packages.txt', serialize($packages)); - if ($this->controller !== null) { - $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); - } - } + if ($this->controller !== null) { + $this->controller->stdout("writing packages file..."); + } + $packages = []; + $notNamespaced = []; + foreach (array_merge($context->classes, $context->interfaces, $context->traits) as $type) { + /** @var TypeDoc $type */ + if (empty($type->namespace)) { + $notNamespaced[] = str_replace('\\', '-', $type->name); + } else { + $packages[$type->namespace][] = str_replace('\\', '-', $type->name); + } + } + ksort($packages); + $packages = array_merge(['Not namespaced' => $notNamespaced], $packages); + foreach ($packages as $name => $classes) { + sort($packages[$name]); + } + file_put_contents($targetDir . '/packages.txt', serialize($packages)); + if ($this->controller !== null) { + $this->controller->stdout('done.' . PHP_EOL, Console::FG_GREEN); + } + } - public function generateApiUrl($typeName) - { - return strtolower(str_replace('\\', '-', $typeName)); - } + public function generateApiUrl($typeName) + { + return strtolower(str_replace('\\', '-', $typeName)); + } - protected function generateFileName($typeName) - { - return $this->generateApiUrl($typeName) . '.html'; - } + protected function generateFileName($typeName) + { + return $this->generateApiUrl($typeName) . '.html'; + } } diff --git a/extensions/apidoc/templates/online/views/index.php b/extensions/apidoc/templates/online/views/index.php index 4f1d2b44cdf..8e60bb54a96 100644 --- a/extensions/apidoc/templates/online/views/index.php +++ b/extensions/apidoc/templates/online/views/index.php @@ -12,22 +12,22 @@ ?>

Class Reference

- - - - - - - - - + + + + + + + + + $class): ?> - - - - + + + +
ClassDescription
ClassDescription
context->typeLink($class, $class->name) ?>shortDescription, $class, true) ?>
context->typeLink($class, $class->name) ?>shortDescription, $class, true) ?>
diff --git a/extensions/authclient/AuthAction.php b/extensions/authclient/AuthAction.php index 0335ba52807..820e41ea274 100644 --- a/extensions/authclient/AuthAction.php +++ b/extensions/authclient/AuthAction.php @@ -56,307 +56,318 @@ */ class AuthAction extends Action { - /** - * @var string name of the auth client collection application component. - * It should point to [[Collection]] instance. - */ - public $clientCollection = 'authClientCollection'; - /** - * @var string name of the GET param, which is used to passed auth client id to this action. - * Note: watch for the naming, make sure you do not choose name used in some auth protocol. - */ - public $clientIdGetParamName = 'authclient'; - /** - * @var callable PHP callback, which should be triggered in case of successful authentication. - * This callback should accept [[ClientInterface]] instance as an argument. - * For example: - * - * ~~~ - * public function onAuthSuccess($client) - * { - * $attributes = $client->getUserAttributes(); - * // user login or signup comes here - * } - * ~~~ - * - * If this callback returns [[Response]] instance, it will be used as action response, - * otherwise redirection to [[successUrl]] will be performed. - * - */ - public $successCallback; - /** - * @var string the redirect url after successful authorization. - */ - private $_successUrl = ''; - /** - * @var string the redirect url after unsuccessful authorization (e.g. user canceled). - */ - private $_cancelUrl = ''; - /** - * @var string name or alias of the view file, which should be rendered in order to perform redirection. - * If not set default one will be used. - */ - public $redirectView; - - /** - * @param string $url successful URL. - */ - public function setSuccessUrl($url) - { - $this->_successUrl = $url; - } - - /** - * @return string successful URL. - */ - public function getSuccessUrl() - { - if (empty($this->_successUrl)) { - $this->_successUrl = $this->defaultSuccessUrl(); - } - return $this->_successUrl; - } - - /** - * @param string $url cancel URL. - */ - public function setCancelUrl($url) - { - $this->_cancelUrl = $url; - } - - /** - * @return string cancel URL. - */ - public function getCancelUrl() - { - if (empty($this->_cancelUrl)) { - $this->_cancelUrl = $this->defaultCancelUrl(); - } - return $this->_cancelUrl; - } - - /** - * Creates default {@link successUrl} value. - * @return string success URL value. - */ - protected function defaultSuccessUrl() - { - return Yii::$app->getUser()->getReturnUrl(); - } - - /** - * Creates default {@link cancelUrl} value. - * @return string cancel URL value. - */ - protected function defaultCancelUrl() - { - return Yii::$app->getRequest()->getAbsoluteUrl(); - } - - /** - * Runs the action. - */ - public function run() - { - if (!empty($_GET[$this->clientIdGetParamName])) { - $clientId = $_GET[$this->clientIdGetParamName]; - /** @var \yii\authclient\Collection $collection */ - $collection = Yii::$app->getComponent($this->clientCollection); - if (!$collection->hasClient($clientId)) { - throw new NotFoundHttpException("Unknown auth client '{$clientId}'"); - } - $client = $collection->getClient($clientId); - return $this->auth($client); - } else { - throw new NotFoundHttpException(); - } - } - - /** - * @param mixed $client auth client instance. - * @return Response response instance. - * @throws \yii\base\NotSupportedException on invalid client. - */ - protected function auth($client) - { - if ($client instanceof OpenId) { - return $this->authOpenId($client); - } elseif ($client instanceof OAuth2) { - return $this->authOAuth2($client); - } elseif ($client instanceof OAuth1) { - return $this->authOAuth1($client); - } else { - throw new NotSupportedException('Provider "' . get_class($client) . '" is not supported.'); - } - } - - /** - * This method is invoked in case of successful authentication via auth client. - * @param ClientInterface $client auth client instance. - * @throws InvalidConfigException on invalid success callback. - * @return Response response instance. - */ - protected function authSuccess($client) - { - if (!is_callable($this->successCallback)) { - throw new InvalidConfigException('"' . get_class($this) . '::successCallback" should be a valid callback.'); - } - $response = call_user_func($this->successCallback, $client); - if ($response instanceof Response) { - return $response; - } - return $this->redirectSuccess(); - } - - /** - * Redirect to the given URL or simply close the popup window. - * @param mixed $url URL to redirect, could be a string or array config to generate a valid URL. - * @param boolean $enforceRedirect indicates if redirect should be performed even in case of popup window. - * @return \yii\web\Response response instance. - */ - public function redirect($url, $enforceRedirect = true) - { - $viewFile = $this->redirectView; - if ($viewFile === null) { - $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; - } else { - $viewFile = Yii::getAlias($viewFile); - } - $viewData = [ - 'url' => $url, - 'enforceRedirect' => $enforceRedirect, - ]; - $response = Yii::$app->getResponse(); - $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); - return $response; - } - - /** - * Redirect to the URL. If URL is null, {@link successUrl} will be used. - * @param string $url URL to redirect. - * @return \yii\web\Response response instance. - */ - public function redirectSuccess($url = null) - { - if ($url === null) { - $url = $this->getSuccessUrl(); - } - return $this->redirect($url); - } - - /** - * Redirect to the {@link cancelUrl} or simply close the popup window. - * @param string $url URL to redirect. - * @return \yii\web\Response response instance. - */ - public function redirectCancel($url = null) - { - if ($url === null) { - $url = $this->getCancelUrl(); - } - return $this->redirect($url, false); - } - - /** - * Performs OpenID auth flow. - * @param OpenId $client auth client instance. - * @return Response action response. - * @throws Exception on failure. - * @throws HttpException on failure. - */ - protected function authOpenId($client) - { - if (!empty($_REQUEST['openid_mode'])) { - switch ($_REQUEST['openid_mode']) { - case 'id_res': - if ($client->validate()) { - return $this->authSuccess($client); - } else { - throw new HttpException(400, 'Unable to complete the authentication because the required data was not received.'); - } - break; - case 'cancel': - $this->redirectCancel(); - break; - default: - throw new HttpException(400); - break; - } - } else { - $url = $client->buildAuthUrl(); - return Yii::$app->getResponse()->redirect($url); - } - return $this->redirectCancel(); - } - - /** - * Performs OAuth1 auth flow. - * @param OAuth1 $client auth client instance. - * @return Response action response. - */ - protected function authOAuth1($client) - { - // user denied error - if (isset($_GET['denied'])) { - return $this->redirectCancel(); - } - - if (isset($_REQUEST['oauth_token'])) { - $oauthToken = $_REQUEST['oauth_token']; - } - - if (!isset($oauthToken)) { - // Get request token. - $requestToken = $client->fetchRequestToken(); - // Get authorization URL. - $url = $client->buildAuthUrl($requestToken); - // Redirect to authorization URL. - return Yii::$app->getResponse()->redirect($url); - } else { - // Upgrade to access token. - $client->fetchAccessToken(); - return $this->authSuccess($client); - } - } - - /** - * Performs OAuth2 auth flow. - * @param OAuth2 $client auth client instance. - * @return Response action response. - * @throws \yii\base\Exception on failure. - */ - protected function authOAuth2($client) - { - if (isset($_GET['error'])) { - if ($_GET['error'] == 'access_denied') { - // user denied error - return $this->redirectCancel(); - } else { - // request error - if (isset($_GET['error_description'])) { - $errorMessage = $_GET['error_description']; - } elseif (isset($_GET['error_message'])) { - $errorMessage = $_GET['error_message']; - } else { - $errorMessage = http_build_query($_GET); - } - throw new Exception('Auth error: ' . $errorMessage); - } - } - - // Get the access_token and save them to the session. - if (isset($_GET['code'])) { - $code = $_GET['code']; - $token = $client->fetchAccessToken($code); - if (!empty($token)) { - return $this->authSuccess($client); - } else { - return $this->redirectCancel(); - } - } else { - $url = $client->buildAuthUrl(); - return Yii::$app->getResponse()->redirect($url); - } - } + /** + * @var string name of the auth client collection application component. + * It should point to [[Collection]] instance. + */ + public $clientCollection = 'authClientCollection'; + /** + * @var string name of the GET param, which is used to passed auth client id to this action. + * Note: watch for the naming, make sure you do not choose name used in some auth protocol. + */ + public $clientIdGetParamName = 'authclient'; + /** + * @var callable PHP callback, which should be triggered in case of successful authentication. + * This callback should accept [[ClientInterface]] instance as an argument. + * For example: + * + * ~~~ + * public function onAuthSuccess($client) + * { + * $attributes = $client->getUserAttributes(); + * // user login or signup comes here + * } + * ~~~ + * + * If this callback returns [[Response]] instance, it will be used as action response, + * otherwise redirection to [[successUrl]] will be performed. + * + */ + public $successCallback; + /** + * @var string the redirect url after successful authorization. + */ + private $_successUrl = ''; + /** + * @var string the redirect url after unsuccessful authorization (e.g. user canceled). + */ + private $_cancelUrl = ''; + /** + * @var string name or alias of the view file, which should be rendered in order to perform redirection. + * If not set default one will be used. + */ + public $redirectView; + + /** + * @param string $url successful URL. + */ + public function setSuccessUrl($url) + { + $this->_successUrl = $url; + } + + /** + * @return string successful URL. + */ + public function getSuccessUrl() + { + if (empty($this->_successUrl)) { + $this->_successUrl = $this->defaultSuccessUrl(); + } + + return $this->_successUrl; + } + + /** + * @param string $url cancel URL. + */ + public function setCancelUrl($url) + { + $this->_cancelUrl = $url; + } + + /** + * @return string cancel URL. + */ + public function getCancelUrl() + { + if (empty($this->_cancelUrl)) { + $this->_cancelUrl = $this->defaultCancelUrl(); + } + + return $this->_cancelUrl; + } + + /** + * Creates default {@link successUrl} value. + * @return string success URL value. + */ + protected function defaultSuccessUrl() + { + return Yii::$app->getUser()->getReturnUrl(); + } + + /** + * Creates default {@link cancelUrl} value. + * @return string cancel URL value. + */ + protected function defaultCancelUrl() + { + return Yii::$app->getRequest()->getAbsoluteUrl(); + } + + /** + * Runs the action. + */ + public function run() + { + if (!empty($_GET[$this->clientIdGetParamName])) { + $clientId = $_GET[$this->clientIdGetParamName]; + /** @var \yii\authclient\Collection $collection */ + $collection = Yii::$app->getComponent($this->clientCollection); + if (!$collection->hasClient($clientId)) { + throw new NotFoundHttpException("Unknown auth client '{$clientId}'"); + } + $client = $collection->getClient($clientId); + + return $this->auth($client); + } else { + throw new NotFoundHttpException(); + } + } + + /** + * @param mixed $client auth client instance. + * @return Response response instance. + * @throws \yii\base\NotSupportedException on invalid client. + */ + protected function auth($client) + { + if ($client instanceof OpenId) { + return $this->authOpenId($client); + } elseif ($client instanceof OAuth2) { + return $this->authOAuth2($client); + } elseif ($client instanceof OAuth1) { + return $this->authOAuth1($client); + } else { + throw new NotSupportedException('Provider "' . get_class($client) . '" is not supported.'); + } + } + + /** + * This method is invoked in case of successful authentication via auth client. + * @param ClientInterface $client auth client instance. + * @throws InvalidConfigException on invalid success callback. + * @return Response response instance. + */ + protected function authSuccess($client) + { + if (!is_callable($this->successCallback)) { + throw new InvalidConfigException('"' . get_class($this) . '::successCallback" should be a valid callback.'); + } + $response = call_user_func($this->successCallback, $client); + if ($response instanceof Response) { + return $response; + } + + return $this->redirectSuccess(); + } + + /** + * Redirect to the given URL or simply close the popup window. + * @param mixed $url URL to redirect, could be a string or array config to generate a valid URL. + * @param boolean $enforceRedirect indicates if redirect should be performed even in case of popup window. + * @return \yii\web\Response response instance. + */ + public function redirect($url, $enforceRedirect = true) + { + $viewFile = $this->redirectView; + if ($viewFile === null) { + $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; + } else { + $viewFile = Yii::getAlias($viewFile); + } + $viewData = [ + 'url' => $url, + 'enforceRedirect' => $enforceRedirect, + ]; + $response = Yii::$app->getResponse(); + $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); + + return $response; + } + + /** + * Redirect to the URL. If URL is null, {@link successUrl} will be used. + * @param string $url URL to redirect. + * @return \yii\web\Response response instance. + */ + public function redirectSuccess($url = null) + { + if ($url === null) { + $url = $this->getSuccessUrl(); + } + + return $this->redirect($url); + } + + /** + * Redirect to the {@link cancelUrl} or simply close the popup window. + * @param string $url URL to redirect. + * @return \yii\web\Response response instance. + */ + public function redirectCancel($url = null) + { + if ($url === null) { + $url = $this->getCancelUrl(); + } + + return $this->redirect($url, false); + } + + /** + * Performs OpenID auth flow. + * @param OpenId $client auth client instance. + * @return Response action response. + * @throws Exception on failure. + * @throws HttpException on failure. + */ + protected function authOpenId($client) + { + if (!empty($_REQUEST['openid_mode'])) { + switch ($_REQUEST['openid_mode']) { + case 'id_res': + if ($client->validate()) { + return $this->authSuccess($client); + } else { + throw new HttpException(400, 'Unable to complete the authentication because the required data was not received.'); + } + break; + case 'cancel': + $this->redirectCancel(); + break; + default: + throw new HttpException(400); + break; + } + } else { + $url = $client->buildAuthUrl(); + + return Yii::$app->getResponse()->redirect($url); + } + + return $this->redirectCancel(); + } + + /** + * Performs OAuth1 auth flow. + * @param OAuth1 $client auth client instance. + * @return Response action response. + */ + protected function authOAuth1($client) + { + // user denied error + if (isset($_GET['denied'])) { + return $this->redirectCancel(); + } + + if (isset($_REQUEST['oauth_token'])) { + $oauthToken = $_REQUEST['oauth_token']; + } + + if (!isset($oauthToken)) { + // Get request token. + $requestToken = $client->fetchRequestToken(); + // Get authorization URL. + $url = $client->buildAuthUrl($requestToken); + // Redirect to authorization URL. + return Yii::$app->getResponse()->redirect($url); + } else { + // Upgrade to access token. + $client->fetchAccessToken(); + + return $this->authSuccess($client); + } + } + + /** + * Performs OAuth2 auth flow. + * @param OAuth2 $client auth client instance. + * @return Response action response. + * @throws \yii\base\Exception on failure. + */ + protected function authOAuth2($client) + { + if (isset($_GET['error'])) { + if ($_GET['error'] == 'access_denied') { + // user denied error + return $this->redirectCancel(); + } else { + // request error + if (isset($_GET['error_description'])) { + $errorMessage = $_GET['error_description']; + } elseif (isset($_GET['error_message'])) { + $errorMessage = $_GET['error_message']; + } else { + $errorMessage = http_build_query($_GET); + } + throw new Exception('Auth error: ' . $errorMessage); + } + } + + // Get the access_token and save them to the session. + if (isset($_GET['code'])) { + $code = $_GET['code']; + $token = $client->fetchAccessToken($code); + if (!empty($token)) { + return $this->authSuccess($client); + } else { + return $this->redirectCancel(); + } + } else { + $url = $client->buildAuthUrl(); + + return Yii::$app->getResponse()->redirect($url); + } + } } diff --git a/extensions/authclient/BaseClient.php b/extensions/authclient/BaseClient.php index f5fe0beb763..86dd0a6dd59 100644 --- a/extensions/authclient/BaseClient.php +++ b/extensions/authclient/BaseClient.php @@ -30,207 +30,214 @@ */ abstract class BaseClient extends Component implements ClientInterface { - /** - * @var string auth service id. - * This value mainly used as HTTP request parameter. - */ - private $_id; - /** - * @var string auth service name. - * This value may be used in database records, CSS files and so on. - */ - private $_name; - /** - * @var string auth service title to display in views. - */ - private $_title; - /** - * @var array authenticated user attributes. - */ - private $_userAttributes; - /** - * @var array map used to normalize user attributes fetched from external auth service - * in format: rawAttributeName => normalizedAttributeName - */ - private $_normalizeUserAttributeMap; - /** - * @var array view options in format: optionName => optionValue - */ - private $_viewOptions; - - /** - * @param string $id service id. - */ - public function setId($id) - { - $this->_id = $id; - } - - /** - * @return string service id - */ - public function getId() - { - if (empty($this->_id)) { - $this->_id = $this->getName(); - } - return $this->_id; - } - - /** - * @param string $name service name. - */ - public function setName($name) - { - $this->_name = $name; - } - - /** - * @return string service name. - */ - public function getName() - { - if ($this->_name === null) { - $this->_name = $this->defaultName(); - } - return $this->_name; - } - - /** - * @param string $title service title. - */ - public function setTitle($title) - { - $this->_title = $title; - } - - /** - * @return string service title. - */ - public function getTitle() - { - if ($this->_title === null) { - $this->_title = $this->defaultTitle(); - } - return $this->_title; - } - - /** - * @param array $userAttributes list of user attributes - */ - public function setUserAttributes($userAttributes) - { - $this->_userAttributes = $this->normalizeUserAttributes($userAttributes); - } - - /** - * @return array list of user attributes - */ - public function getUserAttributes() - { - if ($this->_userAttributes === null) { - $this->_userAttributes = $this->normalizeUserAttributes($this->initUserAttributes()); - } - return $this->_userAttributes; - } - - /** - * @param array $normalizeUserAttributeMap normalize user attribute map. - */ - public function setNormalizeUserAttributeMap($normalizeUserAttributeMap) - { - $this->_normalizeUserAttributeMap = $normalizeUserAttributeMap; - } - - /** - * @return array normalize user attribute map. - */ - public function getNormalizeUserAttributeMap() - { - if ($this->_normalizeUserAttributeMap === null) { - $this->_normalizeUserAttributeMap = $this->defaultNormalizeUserAttributeMap(); - } - return $this->_normalizeUserAttributeMap; - } - - /** - * @param array $viewOptions view options in format: optionName => optionValue - */ - public function setViewOptions($viewOptions) - { - $this->_viewOptions = $viewOptions; - } - - /** - * @return array view options in format: optionName => optionValue - */ - public function getViewOptions() - { - if ($this->_viewOptions === null) { - $this->_viewOptions = $this->defaultViewOptions(); - } - return $this->_viewOptions; - } - - /** - * Generates service name. - * @return string service name. - */ - protected function defaultName() - { - return Inflector::camel2id(StringHelper::basename(get_class($this))); - } - - /** - * Generates service title. - * @return string service title. - */ - protected function defaultTitle() - { - return StringHelper::basename(get_class($this)); - } - - /** - * Initializes authenticated user attributes. - * @return array auth user attributes. - */ - protected function initUserAttributes() - { - throw new NotSupportedException('Method "' . get_class($this) . '::' . __FUNCTION__ . '" not implemented.'); - } - - /** - * Returns the default [[normalizeUserAttributeMap]] value. - * Particular client may override this method in order to provide specific default map. - * @return array normalize attribute map. - */ - protected function defaultNormalizeUserAttributeMap() - { - return []; - } - - /** - * Returns the default [[viewOptions]] value. - * Particular client may override this method in order to provide specific default view options. - * @return array list of default [[viewOptions]] - */ - protected function defaultViewOptions() - { - return []; - } - - /** - * Normalize given user attributes according to {@link normalizeUserAttributeMap}. - * @param array $attributes raw attributes. - * @return array normalized attributes. - */ - protected function normalizeUserAttributes($attributes) - { - foreach ($this->getNormalizeUserAttributeMap() as $normalizedName => $actualName) { - if (array_key_exists($actualName, $attributes)) { - $attributes[$normalizedName] = $attributes[$actualName]; - } - } - return $attributes; - } + /** + * @var string auth service id. + * This value mainly used as HTTP request parameter. + */ + private $_id; + /** + * @var string auth service name. + * This value may be used in database records, CSS files and so on. + */ + private $_name; + /** + * @var string auth service title to display in views. + */ + private $_title; + /** + * @var array authenticated user attributes. + */ + private $_userAttributes; + /** + * @var array map used to normalize user attributes fetched from external auth service + * in format: rawAttributeName => normalizedAttributeName + */ + private $_normalizeUserAttributeMap; + /** + * @var array view options in format: optionName => optionValue + */ + private $_viewOptions; + + /** + * @param string $id service id. + */ + public function setId($id) + { + $this->_id = $id; + } + + /** + * @return string service id + */ + public function getId() + { + if (empty($this->_id)) { + $this->_id = $this->getName(); + } + + return $this->_id; + } + + /** + * @param string $name service name. + */ + public function setName($name) + { + $this->_name = $name; + } + + /** + * @return string service name. + */ + public function getName() + { + if ($this->_name === null) { + $this->_name = $this->defaultName(); + } + + return $this->_name; + } + + /** + * @param string $title service title. + */ + public function setTitle($title) + { + $this->_title = $title; + } + + /** + * @return string service title. + */ + public function getTitle() + { + if ($this->_title === null) { + $this->_title = $this->defaultTitle(); + } + + return $this->_title; + } + + /** + * @param array $userAttributes list of user attributes + */ + public function setUserAttributes($userAttributes) + { + $this->_userAttributes = $this->normalizeUserAttributes($userAttributes); + } + + /** + * @return array list of user attributes + */ + public function getUserAttributes() + { + if ($this->_userAttributes === null) { + $this->_userAttributes = $this->normalizeUserAttributes($this->initUserAttributes()); + } + + return $this->_userAttributes; + } + + /** + * @param array $normalizeUserAttributeMap normalize user attribute map. + */ + public function setNormalizeUserAttributeMap($normalizeUserAttributeMap) + { + $this->_normalizeUserAttributeMap = $normalizeUserAttributeMap; + } + + /** + * @return array normalize user attribute map. + */ + public function getNormalizeUserAttributeMap() + { + if ($this->_normalizeUserAttributeMap === null) { + $this->_normalizeUserAttributeMap = $this->defaultNormalizeUserAttributeMap(); + } + + return $this->_normalizeUserAttributeMap; + } + + /** + * @param array $viewOptions view options in format: optionName => optionValue + */ + public function setViewOptions($viewOptions) + { + $this->_viewOptions = $viewOptions; + } + + /** + * @return array view options in format: optionName => optionValue + */ + public function getViewOptions() + { + if ($this->_viewOptions === null) { + $this->_viewOptions = $this->defaultViewOptions(); + } + + return $this->_viewOptions; + } + + /** + * Generates service name. + * @return string service name. + */ + protected function defaultName() + { + return Inflector::camel2id(StringHelper::basename(get_class($this))); + } + + /** + * Generates service title. + * @return string service title. + */ + protected function defaultTitle() + { + return StringHelper::basename(get_class($this)); + } + + /** + * Initializes authenticated user attributes. + * @return array auth user attributes. + */ + protected function initUserAttributes() + { + throw new NotSupportedException('Method "' . get_class($this) . '::' . __FUNCTION__ . '" not implemented.'); + } + + /** + * Returns the default [[normalizeUserAttributeMap]] value. + * Particular client may override this method in order to provide specific default map. + * @return array normalize attribute map. + */ + protected function defaultNormalizeUserAttributeMap() + { + return []; + } + + /** + * Returns the default [[viewOptions]] value. + * Particular client may override this method in order to provide specific default view options. + * @return array list of default [[viewOptions]] + */ + protected function defaultViewOptions() + { + return []; + } + + /** + * Normalize given user attributes according to {@link normalizeUserAttributeMap}. + * @param array $attributes raw attributes. + * @return array normalized attributes. + */ + protected function normalizeUserAttributes($attributes) + { + foreach ($this->getNormalizeUserAttributeMap() as $normalizedName => $actualName) { + if (array_key_exists($actualName, $attributes)) { + $attributes[$normalizedName] = $attributes[$actualName]; + } + } + + return $attributes; + } } diff --git a/extensions/authclient/BaseOAuth.php b/extensions/authclient/BaseOAuth.php index 66cc6185d06..6ca7dae4827 100644 --- a/extensions/authclient/BaseOAuth.php +++ b/extensions/authclient/BaseOAuth.php @@ -29,482 +29,499 @@ */ abstract class BaseOAuth extends BaseClient implements ClientInterface { - const CONTENT_TYPE_JSON = 'json'; // JSON format - const CONTENT_TYPE_URLENCODED = 'urlencoded'; // urlencoded query string, like name1=value1&name2=value2 - const CONTENT_TYPE_XML = 'xml'; // XML format - const CONTENT_TYPE_AUTO = 'auto'; // attempts to determine format automatically - - /** - * @var string protocol version. - */ - public $version = '1.0'; - /** - * @var string URL, which user will be redirected after authentication at the OAuth provider web site. - * Note: this should be absolute URL (with http:// or https:// leading). - * By default current URL will be used. - */ - private $_returnUrl; - /** - * @var string API base URL. - */ - public $apiBaseUrl; - /** - * @var string authorize URL. - */ - public $authUrl; - /** - * @var string auth request scope. - */ - public $scope; - /** - * @var array cURL request options. Option values from this field will overwrite corresponding - * values from {@link defaultCurlOptions()}. - */ - private $_curlOptions = []; - /** - * @var OAuthToken|array access token instance or its array configuration. - */ - private $_accessToken; - /** - * @var signature\BaseMethod|array signature method instance or its array configuration. - */ - private $_signatureMethod = []; - - /** - * @param string $returnUrl return URL - */ - public function setReturnUrl($returnUrl) - { - $this->_returnUrl = $returnUrl; - } - - /** - * @return string return URL. - */ - public function getReturnUrl() - { - if ($this->_returnUrl === null) { - $this->_returnUrl = $this->defaultReturnUrl(); - } - return $this->_returnUrl; - } - - /** - * @param array $curlOptions cURL options. - */ - public function setCurlOptions(array $curlOptions) - { - $this->_curlOptions = $curlOptions; - } - - /** - * @return array cURL options. - */ - public function getCurlOptions() - { - return $this->_curlOptions; - } - - /** - * @param array|OAuthToken $token - */ - public function setAccessToken($token) - { - if (!is_object($token)) { - $token = $this->createToken($token); - } - $this->_accessToken = $token; - $this->saveAccessToken($token); - } - - /** - * @return OAuthToken auth token instance. - */ - public function getAccessToken() - { - if (!is_object($this->_accessToken)) { - $this->_accessToken = $this->restoreAccessToken(); - } - return $this->_accessToken; - } - - /** - * @param array|signature\BaseMethod $signatureMethod signature method instance or its array configuration. - * @throws InvalidParamException on wrong argument. - */ - public function setSignatureMethod($signatureMethod) - { - if (!is_object($signatureMethod) && !is_array($signatureMethod)) { - throw new InvalidParamException('"' . get_class($this) . '::signatureMethod" should be instance of "\yii\autclient\signature\BaseMethod" or its array configuration. "' . gettype($signatureMethod) . '" has been given.'); - } - $this->_signatureMethod = $signatureMethod; - } - - /** - * @return signature\BaseMethod signature method instance. - */ - public function getSignatureMethod() - { - if (!is_object($this->_signatureMethod)) { - $this->_signatureMethod = $this->createSignatureMethod($this->_signatureMethod); - } - return $this->_signatureMethod; - } - - /** - * Composes default {@link returnUrl} value. - * @return string return URL. - */ - protected function defaultReturnUrl() - { - return Yii::$app->getRequest()->getAbsoluteUrl(); - } - - /** - * Sends HTTP request. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array response. - * @throws Exception on failure. - */ - protected function sendRequest($method, $url, array $params = []) - { - $curlOptions = $this->mergeCurlOptions( - $this->defaultCurlOptions(), - $this->getCurlOptions(), - [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_URL => $url, - ], - $this->composeRequestCurlOptions(strtoupper($method), $url, $params) - ); - $curlResource = curl_init(); - foreach ($curlOptions as $option => $value) { - curl_setopt($curlResource, $option, $value); - } - $response = curl_exec($curlResource); - $responseHeaders = curl_getinfo($curlResource); - - // check cURL error - $errorNumber = curl_errno($curlResource); - $errorMessage = curl_error($curlResource); - - curl_close($curlResource); - - if ($errorNumber > 0) { - throw new Exception('Curl error requesting "' . $url . '": #' . $errorNumber . ' - ' . $errorMessage); - } - if ($responseHeaders['http_code'] != 200) { - throw new Exception('Request failed with code: ' . $responseHeaders['http_code'] . ', message: ' . $response); - } - return $this->processResponse($response, $this->determineContentTypeByHeaders($responseHeaders)); - } - - /** - * Merge CUrl options. - * If each options array has an element with the same key value, the latter - * will overwrite the former. - * @param array $options1 options to be merged to. - * @param array $options2 options to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array merged options (the original options are not changed.) - */ - protected function mergeCurlOptions($options1, $options2) - { - $args = func_get_args(); - $res = array_shift($args); - while (!empty($args)) { - $next = array_shift($args); - foreach ($next as $k => $v) { - $res[$k] = $v; - } - } - return $res; - } - - /** - * Returns default cURL options. - * @return array cURL options. - */ - protected function defaultCurlOptions() - { - return [ - CURLOPT_USERAGENT => Yii::$app->name . ' OAuth ' . $this->version . ' Client', - CURLOPT_CONNECTTIMEOUT => 30, - CURLOPT_TIMEOUT => 30, - CURLOPT_SSL_VERIFYPEER => false, - ]; - } - - /** - * Processes raw response converting it to actual data. - * @param string $rawResponse raw response. - * @param string $contentType response content type. - * @throws Exception on failure. - * @return array actual response. - */ - protected function processResponse($rawResponse, $contentType = self::CONTENT_TYPE_AUTO) - { - if (empty($rawResponse)) { - return []; - } - switch ($contentType) { - case self::CONTENT_TYPE_AUTO: { - $contentType = $this->determineContentTypeByRaw($rawResponse); - if ($contentType == self::CONTENT_TYPE_AUTO) { - throw new Exception('Unable to determine response content type automatically.'); - } - $response = $this->processResponse($rawResponse, $contentType); - break; - } - case self::CONTENT_TYPE_JSON: { - $response = Json::decode($rawResponse, true); - if (isset($response['error'])) { - throw new Exception('Response error: ' . $response['error']); - } - break; - } - case self::CONTENT_TYPE_URLENCODED: { - $response = []; - parse_str($rawResponse, $response); - break; - } - case self::CONTENT_TYPE_XML: { - $response = $this->convertXmlToArray($rawResponse); - break; - } - default: { - throw new Exception('Unknown response type "' . $contentType . '".'); - } - } - return $response; - } - - /** - * Converts XML document to array. - * @param string|\SimpleXMLElement $xml xml to process. - * @return array XML array representation. - */ - protected function convertXmlToArray($xml) - { - if (!is_object($xml)) { - $xml = simplexml_load_string($xml); - } - $result = (array)$xml; - foreach ($result as $key => $value) { - if (is_object($value)) { - $result[$key] = $this->convertXmlToArray($value); - } - } - return $result; - } - - /** - * Attempts to determine HTTP request content type by headers. - * @param array $headers request headers. - * @return string content type. - */ - protected function determineContentTypeByHeaders(array $headers) - { - if (isset($headers['content_type'])) { - if (stripos($headers['content_type'], 'json') !== false) { - return self::CONTENT_TYPE_JSON; - } - if (stripos($headers['content_type'], 'urlencoded') !== false) { - return self::CONTENT_TYPE_URLENCODED; - } - if (stripos($headers['content_type'], 'xml') !== false) { - return self::CONTENT_TYPE_XML; - } - } - return self::CONTENT_TYPE_AUTO; - } - - /** - * Attempts to determine the content type from raw content. - * @param string $rawContent raw response content. - * @return string response type. - */ - protected function determineContentTypeByRaw($rawContent) - { - if (preg_match('/^\\{.*\\}$/is', $rawContent)) { - return self::CONTENT_TYPE_JSON; - } - if (preg_match('/^[^=|^&]+=[^=|^&]+(&[^=|^&]+=[^=|^&]+)*$/is', $rawContent)) { - return self::CONTENT_TYPE_URLENCODED; - } - if (preg_match('/^<.*>$/is', $rawContent)) { - return self::CONTENT_TYPE_XML; - } - return self::CONTENT_TYPE_AUTO; - } - - /** - * Creates signature method instance from its configuration. - * @param array $signatureMethodConfig signature method configuration. - * @return signature\BaseMethod signature method instance. - */ - protected function createSignatureMethod(array $signatureMethodConfig) - { - if (!array_key_exists('class', $signatureMethodConfig)) { - $signatureMethodConfig['class'] = signature\HmacSha1::className(); - } - return Yii::createObject($signatureMethodConfig); - } - - /** - * Creates token from its configuration. - * @param array $tokenConfig token configuration. - * @return OAuthToken token instance. - */ - protected function createToken(array $tokenConfig = []) - { - if (!array_key_exists('class', $tokenConfig)) { - $tokenConfig['class'] = OAuthToken::className(); - } - return Yii::createObject($tokenConfig); - } - - /** - * Composes URL from base URL and GET params. - * @param string $url base URL. - * @param array $params GET params. - * @return string composed URL. - */ - protected function composeUrl($url, array $params = []) - { - if (strpos($url, '?') === false) { - $url .= '?'; - } else { - $url .= '&'; - } - $url .= http_build_query($params, '', '&', PHP_QUERY_RFC3986); - return $url; - } - - /** - * Saves token as persistent state. - * @param OAuthToken $token auth token - * @return static self reference. - */ - protected function saveAccessToken(OAuthToken $token) - { - return $this->setState('token', $token); - } - - /** - * Restores access token. - * @return OAuthToken auth token. - */ - protected function restoreAccessToken() - { - $token = $this->getState('token'); - if (is_object($token)) { - /* @var $token OAuthToken */ - if ($token->getIsExpired()) { - $token = $this->refreshAccessToken($token); - } - } - return $token; - } - - /** - * Sets persistent state. - * @param string $key state key. - * @param mixed $value state value - * @return static self reference. - */ - protected function setState($key, $value) - { - $session = Yii::$app->getSession(); - $key = $this->getStateKeyPrefix() . $key; - $session->set($key, $value); - return $this; - } - - /** - * Returns persistent state value. - * @param string $key state key. - * @return mixed state value. - */ - protected function getState($key) - { - $session = Yii::$app->getSession(); - $key = $this->getStateKeyPrefix() . $key; - $value = $session->get($key); - return $value; - } - - /** - * Removes persistent state value. - * @param string $key state key. - * @return boolean success. - */ - protected function removeState($key) - { - $session = Yii::$app->getSession(); - $key = $this->getStateKeyPrefix() . $key; - $session->remove($key); - return true; - } - - /** - * Returns session key prefix, which is used to store internal states. - * @return string session key prefix. - */ - protected function getStateKeyPrefix() - { - return get_class($this) . '_' . sha1($this->authUrl) . '_'; - } - - /** - * Performs request to the OAuth API. - * @param string $apiSubUrl API sub URL, which will be append to [[apiBaseUrl]], or absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response - * @throws Exception on failure. - */ - public function api($apiSubUrl, $method = 'GET', array $params = []) - { - if (preg_match('/^https?:\\/\\//is', $apiSubUrl)) { - $url = $apiSubUrl; - } else { - $url = $this->apiBaseUrl . '/' . $apiSubUrl; - } - $accessToken = $this->getAccessToken(); - if (!is_object($accessToken) || !$accessToken->getIsValid()) { - throw new Exception('Invalid access token.'); - } - return $this->apiInternal($accessToken, $url, $method, $params); - } - - /** - * Composes HTTP request CUrl options, which will be merged with the default ones. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array CUrl options. - * @throws Exception on failure. - */ - abstract protected function composeRequestCurlOptions($method, $url, array $params); - - /** - * Gets new auth token to replace expired one. - * @param OAuthToken $token expired auth token. - * @return OAuthToken new auth token. - */ - abstract public function refreshAccessToken(OAuthToken $token); - - /** - * Performs request to the OAuth API. - * @param OAuthToken $accessToken actual access token. - * @param string $url absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response. - * @throws Exception on failure. - */ - abstract protected function apiInternal($accessToken, $url, $method, array $params); + const CONTENT_TYPE_JSON = 'json'; // JSON format + const CONTENT_TYPE_URLENCODED = 'urlencoded'; // urlencoded query string, like name1=value1&name2=value2 + const CONTENT_TYPE_XML = 'xml'; // XML format + const CONTENT_TYPE_AUTO = 'auto'; // attempts to determine format automatically + + /** + * @var string protocol version. + */ + public $version = '1.0'; + /** + * @var string URL, which user will be redirected after authentication at the OAuth provider web site. + * Note: this should be absolute URL (with http:// or https:// leading). + * By default current URL will be used. + */ + private $_returnUrl; + /** + * @var string API base URL. + */ + public $apiBaseUrl; + /** + * @var string authorize URL. + */ + public $authUrl; + /** + * @var string auth request scope. + */ + public $scope; + /** + * @var array cURL request options. Option values from this field will overwrite corresponding + * values from {@link defaultCurlOptions()}. + */ + private $_curlOptions = []; + /** + * @var OAuthToken|array access token instance or its array configuration. + */ + private $_accessToken; + /** + * @var signature\BaseMethod|array signature method instance or its array configuration. + */ + private $_signatureMethod = []; + + /** + * @param string $returnUrl return URL + */ + public function setReturnUrl($returnUrl) + { + $this->_returnUrl = $returnUrl; + } + + /** + * @return string return URL. + */ + public function getReturnUrl() + { + if ($this->_returnUrl === null) { + $this->_returnUrl = $this->defaultReturnUrl(); + } + + return $this->_returnUrl; + } + + /** + * @param array $curlOptions cURL options. + */ + public function setCurlOptions(array $curlOptions) + { + $this->_curlOptions = $curlOptions; + } + + /** + * @return array cURL options. + */ + public function getCurlOptions() + { + return $this->_curlOptions; + } + + /** + * @param array|OAuthToken $token + */ + public function setAccessToken($token) + { + if (!is_object($token)) { + $token = $this->createToken($token); + } + $this->_accessToken = $token; + $this->saveAccessToken($token); + } + + /** + * @return OAuthToken auth token instance. + */ + public function getAccessToken() + { + if (!is_object($this->_accessToken)) { + $this->_accessToken = $this->restoreAccessToken(); + } + + return $this->_accessToken; + } + + /** + * @param array|signature\BaseMethod $signatureMethod signature method instance or its array configuration. + * @throws InvalidParamException on wrong argument. + */ + public function setSignatureMethod($signatureMethod) + { + if (!is_object($signatureMethod) && !is_array($signatureMethod)) { + throw new InvalidParamException('"' . get_class($this) . '::signatureMethod" should be instance of "\yii\autclient\signature\BaseMethod" or its array configuration. "' . gettype($signatureMethod) . '" has been given.'); + } + $this->_signatureMethod = $signatureMethod; + } + + /** + * @return signature\BaseMethod signature method instance. + */ + public function getSignatureMethod() + { + if (!is_object($this->_signatureMethod)) { + $this->_signatureMethod = $this->createSignatureMethod($this->_signatureMethod); + } + + return $this->_signatureMethod; + } + + /** + * Composes default {@link returnUrl} value. + * @return string return URL. + */ + protected function defaultReturnUrl() + { + return Yii::$app->getRequest()->getAbsoluteUrl(); + } + + /** + * Sends HTTP request. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array response. + * @throws Exception on failure. + */ + protected function sendRequest($method, $url, array $params = []) + { + $curlOptions = $this->mergeCurlOptions( + $this->defaultCurlOptions(), + $this->getCurlOptions(), + [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_URL => $url, + ], + $this->composeRequestCurlOptions(strtoupper($method), $url, $params) + ); + $curlResource = curl_init(); + foreach ($curlOptions as $option => $value) { + curl_setopt($curlResource, $option, $value); + } + $response = curl_exec($curlResource); + $responseHeaders = curl_getinfo($curlResource); + + // check cURL error + $errorNumber = curl_errno($curlResource); + $errorMessage = curl_error($curlResource); + + curl_close($curlResource); + + if ($errorNumber > 0) { + throw new Exception('Curl error requesting "' . $url . '": #' . $errorNumber . ' - ' . $errorMessage); + } + if ($responseHeaders['http_code'] != 200) { + throw new Exception('Request failed with code: ' . $responseHeaders['http_code'] . ', message: ' . $response); + } + + return $this->processResponse($response, $this->determineContentTypeByHeaders($responseHeaders)); + } + + /** + * Merge CUrl options. + * If each options array has an element with the same key value, the latter + * will overwrite the former. + * @param array $options1 options to be merged to. + * @param array $options2 options to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array merged options (the original options are not changed.) + */ + protected function mergeCurlOptions($options1, $options2) + { + $args = func_get_args(); + $res = array_shift($args); + while (!empty($args)) { + $next = array_shift($args); + foreach ($next as $k => $v) { + $res[$k] = $v; + } + } + + return $res; + } + + /** + * Returns default cURL options. + * @return array cURL options. + */ + protected function defaultCurlOptions() + { + return [ + CURLOPT_USERAGENT => Yii::$app->name . ' OAuth ' . $this->version . ' Client', + CURLOPT_CONNECTTIMEOUT => 30, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + ]; + } + + /** + * Processes raw response converting it to actual data. + * @param string $rawResponse raw response. + * @param string $contentType response content type. + * @throws Exception on failure. + * @return array actual response. + */ + protected function processResponse($rawResponse, $contentType = self::CONTENT_TYPE_AUTO) + { + if (empty($rawResponse)) { + return []; + } + switch ($contentType) { + case self::CONTENT_TYPE_AUTO: { + $contentType = $this->determineContentTypeByRaw($rawResponse); + if ($contentType == self::CONTENT_TYPE_AUTO) { + throw new Exception('Unable to determine response content type automatically.'); + } + $response = $this->processResponse($rawResponse, $contentType); + break; + } + case self::CONTENT_TYPE_JSON: { + $response = Json::decode($rawResponse, true); + if (isset($response['error'])) { + throw new Exception('Response error: ' . $response['error']); + } + break; + } + case self::CONTENT_TYPE_URLENCODED: { + $response = []; + parse_str($rawResponse, $response); + break; + } + case self::CONTENT_TYPE_XML: { + $response = $this->convertXmlToArray($rawResponse); + break; + } + default: { + throw new Exception('Unknown response type "' . $contentType . '".'); + } + } + + return $response; + } + + /** + * Converts XML document to array. + * @param string|\SimpleXMLElement $xml xml to process. + * @return array XML array representation. + */ + protected function convertXmlToArray($xml) + { + if (!is_object($xml)) { + $xml = simplexml_load_string($xml); + } + $result = (array) $xml; + foreach ($result as $key => $value) { + if (is_object($value)) { + $result[$key] = $this->convertXmlToArray($value); + } + } + + return $result; + } + + /** + * Attempts to determine HTTP request content type by headers. + * @param array $headers request headers. + * @return string content type. + */ + protected function determineContentTypeByHeaders(array $headers) + { + if (isset($headers['content_type'])) { + if (stripos($headers['content_type'], 'json') !== false) { + return self::CONTENT_TYPE_JSON; + } + if (stripos($headers['content_type'], 'urlencoded') !== false) { + return self::CONTENT_TYPE_URLENCODED; + } + if (stripos($headers['content_type'], 'xml') !== false) { + return self::CONTENT_TYPE_XML; + } + } + + return self::CONTENT_TYPE_AUTO; + } + + /** + * Attempts to determine the content type from raw content. + * @param string $rawContent raw response content. + * @return string response type. + */ + protected function determineContentTypeByRaw($rawContent) + { + if (preg_match('/^\\{.*\\}$/is', $rawContent)) { + return self::CONTENT_TYPE_JSON; + } + if (preg_match('/^[^=|^&]+=[^=|^&]+(&[^=|^&]+=[^=|^&]+)*$/is', $rawContent)) { + return self::CONTENT_TYPE_URLENCODED; + } + if (preg_match('/^<.*>$/is', $rawContent)) { + return self::CONTENT_TYPE_XML; + } + + return self::CONTENT_TYPE_AUTO; + } + + /** + * Creates signature method instance from its configuration. + * @param array $signatureMethodConfig signature method configuration. + * @return signature\BaseMethod signature method instance. + */ + protected function createSignatureMethod(array $signatureMethodConfig) + { + if (!array_key_exists('class', $signatureMethodConfig)) { + $signatureMethodConfig['class'] = signature\HmacSha1::className(); + } + + return Yii::createObject($signatureMethodConfig); + } + + /** + * Creates token from its configuration. + * @param array $tokenConfig token configuration. + * @return OAuthToken token instance. + */ + protected function createToken(array $tokenConfig = []) + { + if (!array_key_exists('class', $tokenConfig)) { + $tokenConfig['class'] = OAuthToken::className(); + } + + return Yii::createObject($tokenConfig); + } + + /** + * Composes URL from base URL and GET params. + * @param string $url base URL. + * @param array $params GET params. + * @return string composed URL. + */ + protected function composeUrl($url, array $params = []) + { + if (strpos($url, '?') === false) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + return $url; + } + + /** + * Saves token as persistent state. + * @param OAuthToken $token auth token + * @return static self reference. + */ + protected function saveAccessToken(OAuthToken $token) + { + return $this->setState('token', $token); + } + + /** + * Restores access token. + * @return OAuthToken auth token. + */ + protected function restoreAccessToken() + { + $token = $this->getState('token'); + if (is_object($token)) { + /* @var $token OAuthToken */ + if ($token->getIsExpired()) { + $token = $this->refreshAccessToken($token); + } + } + + return $token; + } + + /** + * Sets persistent state. + * @param string $key state key. + * @param mixed $value state value + * @return static self reference. + */ + protected function setState($key, $value) + { + $session = Yii::$app->getSession(); + $key = $this->getStateKeyPrefix() . $key; + $session->set($key, $value); + + return $this; + } + + /** + * Returns persistent state value. + * @param string $key state key. + * @return mixed state value. + */ + protected function getState($key) + { + $session = Yii::$app->getSession(); + $key = $this->getStateKeyPrefix() . $key; + $value = $session->get($key); + + return $value; + } + + /** + * Removes persistent state value. + * @param string $key state key. + * @return boolean success. + */ + protected function removeState($key) + { + $session = Yii::$app->getSession(); + $key = $this->getStateKeyPrefix() . $key; + $session->remove($key); + + return true; + } + + /** + * Returns session key prefix, which is used to store internal states. + * @return string session key prefix. + */ + protected function getStateKeyPrefix() + { + return get_class($this) . '_' . sha1($this->authUrl) . '_'; + } + + /** + * Performs request to the OAuth API. + * @param string $apiSubUrl API sub URL, which will be append to [[apiBaseUrl]], or absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response + * @throws Exception on failure. + */ + public function api($apiSubUrl, $method = 'GET', array $params = []) + { + if (preg_match('/^https?:\\/\\//is', $apiSubUrl)) { + $url = $apiSubUrl; + } else { + $url = $this->apiBaseUrl . '/' . $apiSubUrl; + } + $accessToken = $this->getAccessToken(); + if (!is_object($accessToken) || !$accessToken->getIsValid()) { + throw new Exception('Invalid access token.'); + } + + return $this->apiInternal($accessToken, $url, $method, $params); + } + + /** + * Composes HTTP request CUrl options, which will be merged with the default ones. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array CUrl options. + * @throws Exception on failure. + */ + abstract protected function composeRequestCurlOptions($method, $url, array $params); + + /** + * Gets new auth token to replace expired one. + * @param OAuthToken $token expired auth token. + * @return OAuthToken new auth token. + */ + abstract public function refreshAccessToken(OAuthToken $token); + + /** + * Performs request to the OAuth API. + * @param OAuthToken $accessToken actual access token. + * @param string $url absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response. + * @throws Exception on failure. + */ + abstract protected function apiInternal($accessToken, $url, $method, array $params); } diff --git a/extensions/authclient/ClientInterface.php b/extensions/authclient/ClientInterface.php index 0c46d26e09d..54c70bd10e3 100644 --- a/extensions/authclient/ClientInterface.php +++ b/extensions/authclient/ClientInterface.php @@ -15,43 +15,43 @@ */ interface ClientInterface { - /** - * @param string $id service id. - */ - public function setId($id); - - /** - * @return string service id - */ - public function getId(); - - /** - * @return string service name. - */ - public function getName(); - - /** - * @param string $name service name. - */ - public function setName($name); - - /** - * @return string service title. - */ - public function getTitle(); - - /** - * @param string $title service title. - */ - public function setTitle($title); - - /** - * @return array list of user attributes - */ - public function getUserAttributes(); - - /** - * @return array view options in format: optionName => optionValue - */ - public function getViewOptions(); + /** + * @param string $id service id. + */ + public function setId($id); + + /** + * @return string service id + */ + public function getId(); + + /** + * @return string service name. + */ + public function getName(); + + /** + * @param string $name service name. + */ + public function setName($name); + + /** + * @return string service title. + */ + public function getTitle(); + + /** + * @param string $title service title. + */ + public function setTitle($title); + + /** + * @return array list of user attributes + */ + public function getUserAttributes(); + + /** + * @return array view options in format: optionName => optionValue + */ + public function getViewOptions(); } diff --git a/extensions/authclient/Collection.php b/extensions/authclient/Collection.php index 2573cf4ce6c..e4a03a9bde9 100644 --- a/extensions/authclient/Collection.php +++ b/extensions/authclient/Collection.php @@ -42,66 +42,69 @@ */ class Collection extends Component { - /** - * @var array list of Auth clients with their configuration in format: 'clientId' => [...] - */ - private $_clients = []; + /** + * @var array list of Auth clients with their configuration in format: 'clientId' => [...] + */ + private $_clients = []; - /** - * @param array $clients list of auth clients - */ - public function setClients(array $clients) - { - $this->_clients = $clients; - } + /** + * @param array $clients list of auth clients + */ + public function setClients(array $clients) + { + $this->_clients = $clients; + } - /** - * @return ClientInterface[] list of auth clients. - */ - public function getClients() - { - $clients = []; - foreach ($this->_clients as $id => $client) { - $clients[$id] = $this->getClient($id); - } - return $clients; - } + /** + * @return ClientInterface[] list of auth clients. + */ + public function getClients() + { + $clients = []; + foreach ($this->_clients as $id => $client) { + $clients[$id] = $this->getClient($id); + } - /** - * @param string $id service id. - * @return ClientInterface auth client instance. - * @throws InvalidParamException on non existing client request. - */ - public function getClient($id) - { - if (!array_key_exists($id, $this->_clients)) { - throw new InvalidParamException("Unknown auth client '{$id}'."); - } - if (!is_object($this->_clients[$id])) { - $this->_clients[$id] = $this->createClient($id, $this->_clients[$id]); - } - return $this->_clients[$id]; - } + return $clients; + } - /** - * Checks if client exists in the hub. - * @param string $id client id. - * @return boolean whether client exist. - */ - public function hasClient($id) - { - return array_key_exists($id, $this->_clients); - } + /** + * @param string $id service id. + * @return ClientInterface auth client instance. + * @throws InvalidParamException on non existing client request. + */ + public function getClient($id) + { + if (!array_key_exists($id, $this->_clients)) { + throw new InvalidParamException("Unknown auth client '{$id}'."); + } + if (!is_object($this->_clients[$id])) { + $this->_clients[$id] = $this->createClient($id, $this->_clients[$id]); + } - /** - * Creates auth client instance from its array configuration. - * @param string $id auth client id. - * @param array $config auth client instance configuration. - * @return ClientInterface auth client instance. - */ - protected function createClient($id, $config) - { - $config['id'] = $id; - return Yii::createObject($config); - } + return $this->_clients[$id]; + } + + /** + * Checks if client exists in the hub. + * @param string $id client id. + * @return boolean whether client exist. + */ + public function hasClient($id) + { + return array_key_exists($id, $this->_clients); + } + + /** + * Creates auth client instance from its array configuration. + * @param string $id auth client id. + * @param array $config auth client instance configuration. + * @return ClientInterface auth client instance. + */ + protected function createClient($id, $config) + { + $config['id'] = $id; + + return Yii::createObject($config); + } } diff --git a/extensions/authclient/OAuth1.php b/extensions/authclient/OAuth1.php index 9937a76cf0c..329e0eca420 100644 --- a/extensions/authclient/OAuth1.php +++ b/extensions/authclient/OAuth1.php @@ -33,323 +33,335 @@ */ class OAuth1 extends BaseOAuth { - /** - * @var string protocol version. - */ - public $version = '1.0'; - /** - * @var string OAuth consumer key. - */ - public $consumerKey; - /** - * @var string OAuth consumer secret. - */ - public $consumerSecret; - /** - * @var string OAuth request token URL. - */ - public $requestTokenUrl; - /** - * @var string request token HTTP method. - */ - public $requestTokenMethod = 'GET'; - /** - * @var string OAuth access token URL. - */ - public $accessTokenUrl; - /** - * @var string access token HTTP method. - */ - public $accessTokenMethod = 'GET'; + /** + * @var string protocol version. + */ + public $version = '1.0'; + /** + * @var string OAuth consumer key. + */ + public $consumerKey; + /** + * @var string OAuth consumer secret. + */ + public $consumerSecret; + /** + * @var string OAuth request token URL. + */ + public $requestTokenUrl; + /** + * @var string request token HTTP method. + */ + public $requestTokenMethod = 'GET'; + /** + * @var string OAuth access token URL. + */ + public $accessTokenUrl; + /** + * @var string access token HTTP method. + */ + public $accessTokenMethod = 'GET'; - /** - * Fetches the OAuth request token. - * @param array $params additional request params. - * @return OAuthToken request token. - */ - public function fetchRequestToken(array $params = []) - { - $this->removeState('token'); - $defaultParams = [ - 'oauth_consumer_key' => $this->consumerKey, - 'oauth_callback' => $this->getReturnUrl(), - //'xoauth_displayname' => Yii::$app->name, - ]; - if (!empty($this->scope)) { - $defaultParams['scope'] = $this->scope; - } - $response = $this->sendSignedRequest($this->requestTokenMethod, $this->requestTokenUrl, array_merge($defaultParams, $params)); - $token = $this->createToken([ - 'params' => $response - ]); - $this->setState('requestToken', $token); - return $token; - } + /** + * Fetches the OAuth request token. + * @param array $params additional request params. + * @return OAuthToken request token. + */ + public function fetchRequestToken(array $params = []) + { + $this->removeState('token'); + $defaultParams = [ + 'oauth_consumer_key' => $this->consumerKey, + 'oauth_callback' => $this->getReturnUrl(), + //'xoauth_displayname' => Yii::$app->name, + ]; + if (!empty($this->scope)) { + $defaultParams['scope'] = $this->scope; + } + $response = $this->sendSignedRequest($this->requestTokenMethod, $this->requestTokenUrl, array_merge($defaultParams, $params)); + $token = $this->createToken([ + 'params' => $response + ]); + $this->setState('requestToken', $token); - /** - * Composes user authorization URL. - * @param OAuthToken $requestToken OAuth request token. - * @param array $params additional request params. - * @return string authorize URL - * @throws Exception on failure. - */ - public function buildAuthUrl(OAuthToken $requestToken = null, array $params = []) - { - if (!is_object($requestToken)) { - $requestToken = $this->getState('requestToken'); - if (!is_object($requestToken)) { - throw new Exception('Request token is required to build authorize URL!'); - } - } - $params['oauth_token'] = $requestToken->getToken(); - return $this->composeUrl($this->authUrl, $params); - } + return $token; + } - /** - * Fetches OAuth access token. - * @param OAuthToken $requestToken OAuth request token. - * @param string $oauthVerifier OAuth verifier. - * @param array $params additional request params. - * @return OAuthToken OAuth access token. - * @throws Exception on failure. - */ - public function fetchAccessToken(OAuthToken $requestToken = null, $oauthVerifier = null, array $params = []) - { - if (!is_object($requestToken)) { - $requestToken = $this->getState('requestToken'); - if (!is_object($requestToken)) { - throw new Exception('Request token is required to fetch access token!'); - } - } - $this->removeState('requestToken'); - $defaultParams = [ - 'oauth_consumer_key' => $this->consumerKey, - 'oauth_token' => $requestToken->getToken() - ]; - if ($oauthVerifier === null) { - if (isset($_REQUEST['oauth_verifier'])) { - $oauthVerifier = $_REQUEST['oauth_verifier']; - } - } - if (!empty($oauthVerifier)) { - $defaultParams['oauth_verifier'] = $oauthVerifier; - } - $response = $this->sendSignedRequest($this->accessTokenMethod, $this->accessTokenUrl, array_merge($defaultParams, $params)); + /** + * Composes user authorization URL. + * @param OAuthToken $requestToken OAuth request token. + * @param array $params additional request params. + * @return string authorize URL + * @throws Exception on failure. + */ + public function buildAuthUrl(OAuthToken $requestToken = null, array $params = []) + { + if (!is_object($requestToken)) { + $requestToken = $this->getState('requestToken'); + if (!is_object($requestToken)) { + throw new Exception('Request token is required to build authorize URL!'); + } + } + $params['oauth_token'] = $requestToken->getToken(); - $token = $this->createToken([ - 'params' => $response - ]); - $this->setAccessToken($token); - return $token; - } + return $this->composeUrl($this->authUrl, $params); + } - /** - * Sends HTTP request, signed by {@link signatureMethod}. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array response. - */ - protected function sendSignedRequest($method, $url, array $params = []) - { - $params = array_merge($params, $this->generateCommonRequestParams()); - $params = $this->signRequest($method, $url, $params); - return $this->sendRequest($method, $url, $params); - } + /** + * Fetches OAuth access token. + * @param OAuthToken $requestToken OAuth request token. + * @param string $oauthVerifier OAuth verifier. + * @param array $params additional request params. + * @return OAuthToken OAuth access token. + * @throws Exception on failure. + */ + public function fetchAccessToken(OAuthToken $requestToken = null, $oauthVerifier = null, array $params = []) + { + if (!is_object($requestToken)) { + $requestToken = $this->getState('requestToken'); + if (!is_object($requestToken)) { + throw new Exception('Request token is required to fetch access token!'); + } + } + $this->removeState('requestToken'); + $defaultParams = [ + 'oauth_consumer_key' => $this->consumerKey, + 'oauth_token' => $requestToken->getToken() + ]; + if ($oauthVerifier === null) { + if (isset($_REQUEST['oauth_verifier'])) { + $oauthVerifier = $_REQUEST['oauth_verifier']; + } + } + if (!empty($oauthVerifier)) { + $defaultParams['oauth_verifier'] = $oauthVerifier; + } + $response = $this->sendSignedRequest($this->accessTokenMethod, $this->accessTokenUrl, array_merge($defaultParams, $params)); - /** - * Composes HTTP request CUrl options, which will be merged with the default ones. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array CUrl options. - * @throws Exception on failure. - */ - protected function composeRequestCurlOptions($method, $url, array $params) - { - $curlOptions = []; - switch ($method) { - case 'GET': { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - break; - } - case 'POST': { - $curlOptions[CURLOPT_POST] = true; - if (!empty($params)) { - $curlOptions[CURLOPT_POSTFIELDS] = $params; - } - $authorizationHeader = $this->composeAuthorizationHeader($params); - if (!empty($authorizationHeader)) { - $curlOptions[CURLOPT_HTTPHEADER] = ['Content-Type: application/atom+xml', $authorizationHeader]; - } - break; - } - case 'HEAD': - case 'PUT': - case 'DELETE': { - $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; - if (!empty($params)) { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - } - break; - } - default: { - throw new Exception("Unknown request method '{$method}'."); - } - } - return $curlOptions; - } + $token = $this->createToken([ + 'params' => $response + ]); + $this->setAccessToken($token); - /** - * Performs request to the OAuth API. - * @param OAuthToken $accessToken actual access token. - * @param string $url absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response. - * @throws Exception on failure. - */ - protected function apiInternal($accessToken, $url, $method, array $params) - { - $params['oauth_consumer_key'] = $this->consumerKey; - $params['oauth_token'] = $accessToken->getToken(); - $response = $this->sendSignedRequest($method, $url, $params); - return $response; - } + return $token; + } - /** - * Gets new auth token to replace expired one. - * @param OAuthToken $token expired auth token. - * @return OAuthToken new auth token. - */ - public function refreshAccessToken(OAuthToken $token) - { - // @todo - return null; - } + /** + * Sends HTTP request, signed by {@link signatureMethod}. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array response. + */ + protected function sendSignedRequest($method, $url, array $params = []) + { + $params = array_merge($params, $this->generateCommonRequestParams()); + $params = $this->signRequest($method, $url, $params); - /** - * Composes default {@link returnUrl} value. - * @return string return URL. - */ - protected function defaultReturnUrl() - { - $params = $_GET; - unset($params['oauth_token']); - $params[0] = Yii::$app->controller->getRoute(); - return Yii::$app->getUrlManager()->createAbsoluteUrl($params); - } + return $this->sendRequest($method, $url, $params); + } - /** - * Generates nonce value. - * @return string nonce value. - */ - protected function generateNonce() - { - return md5(microtime() . mt_rand()); - } + /** + * Composes HTTP request CUrl options, which will be merged with the default ones. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array CUrl options. + * @throws Exception on failure. + */ + protected function composeRequestCurlOptions($method, $url, array $params) + { + $curlOptions = []; + switch ($method) { + case 'GET': { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + break; + } + case 'POST': { + $curlOptions[CURLOPT_POST] = true; + if (!empty($params)) { + $curlOptions[CURLOPT_POSTFIELDS] = $params; + } + $authorizationHeader = $this->composeAuthorizationHeader($params); + if (!empty($authorizationHeader)) { + $curlOptions[CURLOPT_HTTPHEADER] = ['Content-Type: application/atom+xml', $authorizationHeader]; + } + break; + } + case 'HEAD': + case 'PUT': + case 'DELETE': { + $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; + if (!empty($params)) { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + } + break; + } + default: { + throw new Exception("Unknown request method '{$method}'."); + } + } - /** - * Generates timestamp. - * @return integer timestamp. - */ - protected function generateTimestamp() - { - return time(); - } + return $curlOptions; + } - /** - * Generate common request params like version, timestamp etc. - * @return array common request params. - */ - protected function generateCommonRequestParams() - { - $params = [ - 'oauth_version' => $this->version, - 'oauth_nonce' => $this->generateNonce(), - 'oauth_timestamp' => $this->generateTimestamp(), - ]; - return $params; - } + /** + * Performs request to the OAuth API. + * @param OAuthToken $accessToken actual access token. + * @param string $url absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response. + * @throws Exception on failure. + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + $params['oauth_consumer_key'] = $this->consumerKey; + $params['oauth_token'] = $accessToken->getToken(); + $response = $this->sendSignedRequest($method, $url, $params); - /** - * Sign request with {@link signatureMethod}. - * @param string $method request method. - * @param string $url request URL. - * @param array $params request params. - * @return array signed request params. - */ - protected function signRequest($method, $url, array $params) - { - $signatureMethod = $this->getSignatureMethod(); - $params['oauth_signature_method'] = $signatureMethod->getName(); - $signatureBaseString = $this->composeSignatureBaseString($method, $url, $params); - $signatureKey = $this->composeSignatureKey(); - $params['oauth_signature'] = $signatureMethod->generateSignature($signatureBaseString, $signatureKey); - return $params; - } + return $response; + } - /** - * Creates signature base string, which will be signed by {@link signatureMethod}. - * @param string $method request method. - * @param string $url request URL. - * @param array $params request params. - * @return string base signature string. - */ - protected function composeSignatureBaseString($method, $url, array $params) - { - unset($params['oauth_signature']); - uksort($params, 'strcmp'); // Parameters are sorted by name, using lexicographical byte value ordering. Ref: Spec: 9.1.1 - $parts = [ - strtoupper($method), - $url, - http_build_query($params, '', '&', PHP_QUERY_RFC3986) - ]; - $parts = array_map('rawurlencode', $parts); - return implode('&', $parts); - } + /** + * Gets new auth token to replace expired one. + * @param OAuthToken $token expired auth token. + * @return OAuthToken new auth token. + */ + public function refreshAccessToken(OAuthToken $token) + { + // @todo + return null; + } - /** - * Composes request signature key. - * @return string signature key. - */ - protected function composeSignatureKey() - { - $signatureKeyParts = [ - $this->consumerSecret - ]; - $accessToken = $this->getAccessToken(); - if (is_object($accessToken)) { - $signatureKeyParts[] = $accessToken->getTokenSecret(); - } else { - $signatureKeyParts[] = ''; - } - $signatureKeyParts = array_map('rawurlencode', $signatureKeyParts); - return implode('&', $signatureKeyParts); - } + /** + * Composes default {@link returnUrl} value. + * @return string return URL. + */ + protected function defaultReturnUrl() + { + $params = $_GET; + unset($params['oauth_token']); + $params[0] = Yii::$app->controller->getRoute(); - /** - * Composes authorization header content. - * @param array $params request params. - * @param string $realm authorization realm. - * @return string authorization header content. - */ - protected function composeAuthorizationHeader(array $params, $realm = '') - { - $header = 'Authorization: OAuth'; - $headerParams = []; - if (!empty($realm)) { - $headerParams[] = 'realm="' . rawurlencode($realm) . '"'; - } - foreach ($params as $key => $value) { - if (substr($key, 0, 5) != 'oauth') { - continue; - } - $headerParams[] = rawurlencode($key) . '="' . rawurlencode($value) . '"'; - } - if (!empty($headerParams)) { - $header .= ' ' . implode(', ', $headerParams); - } - return $header; - } + return Yii::$app->getUrlManager()->createAbsoluteUrl($params); + } + + /** + * Generates nonce value. + * @return string nonce value. + */ + protected function generateNonce() + { + return md5(microtime() . mt_rand()); + } + + /** + * Generates timestamp. + * @return integer timestamp. + */ + protected function generateTimestamp() + { + return time(); + } + + /** + * Generate common request params like version, timestamp etc. + * @return array common request params. + */ + protected function generateCommonRequestParams() + { + $params = [ + 'oauth_version' => $this->version, + 'oauth_nonce' => $this->generateNonce(), + 'oauth_timestamp' => $this->generateTimestamp(), + ]; + + return $params; + } + + /** + * Sign request with {@link signatureMethod}. + * @param string $method request method. + * @param string $url request URL. + * @param array $params request params. + * @return array signed request params. + */ + protected function signRequest($method, $url, array $params) + { + $signatureMethod = $this->getSignatureMethod(); + $params['oauth_signature_method'] = $signatureMethod->getName(); + $signatureBaseString = $this->composeSignatureBaseString($method, $url, $params); + $signatureKey = $this->composeSignatureKey(); + $params['oauth_signature'] = $signatureMethod->generateSignature($signatureBaseString, $signatureKey); + + return $params; + } + + /** + * Creates signature base string, which will be signed by {@link signatureMethod}. + * @param string $method request method. + * @param string $url request URL. + * @param array $params request params. + * @return string base signature string. + */ + protected function composeSignatureBaseString($method, $url, array $params) + { + unset($params['oauth_signature']); + uksort($params, 'strcmp'); // Parameters are sorted by name, using lexicographical byte value ordering. Ref: Spec: 9.1.1 + $parts = [ + strtoupper($method), + $url, + http_build_query($params, '', '&', PHP_QUERY_RFC3986) + ]; + $parts = array_map('rawurlencode', $parts); + + return implode('&', $parts); + } + + /** + * Composes request signature key. + * @return string signature key. + */ + protected function composeSignatureKey() + { + $signatureKeyParts = [ + $this->consumerSecret + ]; + $accessToken = $this->getAccessToken(); + if (is_object($accessToken)) { + $signatureKeyParts[] = $accessToken->getTokenSecret(); + } else { + $signatureKeyParts[] = ''; + } + $signatureKeyParts = array_map('rawurlencode', $signatureKeyParts); + + return implode('&', $signatureKeyParts); + } + + /** + * Composes authorization header content. + * @param array $params request params. + * @param string $realm authorization realm. + * @return string authorization header content. + */ + protected function composeAuthorizationHeader(array $params, $realm = '') + { + $header = 'Authorization: OAuth'; + $headerParams = []; + if (!empty($realm)) { + $headerParams[] = 'realm="' . rawurlencode($realm) . '"'; + } + foreach ($params as $key => $value) { + if (substr($key, 0, 5) != 'oauth') { + continue; + } + $headerParams[] = rawurlencode($key) . '="' . rawurlencode($value) . '"'; + } + if (!empty($headerParams)) { + $header .= ' ' . implode(', ', $headerParams); + } + + return $header; + } } diff --git a/extensions/authclient/OAuth2.php b/extensions/authclient/OAuth2.php index c40ad16ef5a..518154bec48 100644 --- a/extensions/authclient/OAuth2.php +++ b/extensions/authclient/OAuth2.php @@ -33,153 +33,160 @@ */ class OAuth2 extends BaseOAuth { - /** - * @var string protocol version. - */ - public $version = '2.0'; - /** - * @var string OAuth client ID. - */ - public $clientId; - /** - * @var string OAuth client secret. - */ - public $clientSecret; - /** - * @var string token request URL endpoint. - */ - public $tokenUrl; - - /** - * Composes user authorization URL. - * @param array $params additional auth GET params. - * @return string authorization URL. - */ - public function buildAuthUrl(array $params = []) - { - $defaultParams = [ - 'client_id' => $this->clientId, - 'response_type' => 'code', - 'redirect_uri' => $this->getReturnUrl(), - 'xoauth_displayname' => Yii::$app->name, - ]; - if (!empty($this->scope)) { - $defaultParams['scope'] = $this->scope; - } - return $this->composeUrl($this->authUrl, array_merge($defaultParams, $params)); - } - - /** - * Fetches access token from authorization code. - * @param string $authCode authorization code, usually comes at $_GET['code']. - * @param array $params additional request params. - * @return OAuthToken access token. - */ - public function fetchAccessToken($authCode, array $params = []) - { - $defaultParams = [ - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - 'code' => $authCode, - 'grant_type' => 'authorization_code', - 'redirect_uri' => $this->getReturnUrl(), - ]; - $response = $this->sendRequest('POST', $this->tokenUrl, array_merge($defaultParams, $params)); - $token = $this->createToken(['params' => $response]); - $this->setAccessToken($token); - return $token; - } - - /** - * Composes HTTP request CUrl options, which will be merged with the default ones. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array CUrl options. - * @throws Exception on failure. - */ - protected function composeRequestCurlOptions($method, $url, array $params) - { - $curlOptions = []; - switch ($method) { - case 'GET': { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - break; - } - case 'POST': { - $curlOptions[CURLOPT_POST] = true; - $curlOptions[CURLOPT_HTTPHEADER] = ['Content-type: application/x-www-form-urlencoded']; - $curlOptions[CURLOPT_POSTFIELDS] = http_build_query($params, '', '&', PHP_QUERY_RFC3986); - break; - } - case 'HEAD': - case 'PUT': - case 'DELETE': { - $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; - if (!empty($params)) { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - } - break; - } - default: { - throw new Exception("Unknown request method '{$method}'."); - } - } - return $curlOptions; - } - - /** - * Performs request to the OAuth API. - * @param OAuthToken $accessToken actual access token. - * @param string $url absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response. - * @throws Exception on failure. - */ - protected function apiInternal($accessToken, $url, $method, array $params) - { - $params['access_token'] = $accessToken->getToken(); - return $this->sendRequest($method, $url, $params); - } - - /** - * Gets new auth token to replace expired one. - * @param OAuthToken $token expired auth token. - * @return OAuthToken new auth token. - */ - public function refreshAccessToken(OAuthToken $token) - { - $params = [ - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - 'grant_type' => 'refresh_token' - ]; - $params = array_merge($token->getParams(), $params); - $response = $this->sendRequest('POST', $this->tokenUrl, $params); - return $response; - } - - /** - * Composes default {@link returnUrl} value. - * @return string return URL. - */ - protected function defaultReturnUrl() - { - $params = $_GET; - unset($params['code']); - $params[0] = Yii::$app->controller->getRoute(); - return Yii::$app->getUrlManager()->createAbsoluteUrl($params); - } - - /** - * Creates token from its configuration. - * @param array $tokenConfig token configuration. - * @return OAuthToken token instance. - */ - protected function createToken(array $tokenConfig = []) - { - $tokenConfig['tokenParamKey'] = 'access_token'; - return parent::createToken($tokenConfig); - } + /** + * @var string protocol version. + */ + public $version = '2.0'; + /** + * @var string OAuth client ID. + */ + public $clientId; + /** + * @var string OAuth client secret. + */ + public $clientSecret; + /** + * @var string token request URL endpoint. + */ + public $tokenUrl; + + /** + * Composes user authorization URL. + * @param array $params additional auth GET params. + * @return string authorization URL. + */ + public function buildAuthUrl(array $params = []) + { + $defaultParams = [ + 'client_id' => $this->clientId, + 'response_type' => 'code', + 'redirect_uri' => $this->getReturnUrl(), + 'xoauth_displayname' => Yii::$app->name, + ]; + if (!empty($this->scope)) { + $defaultParams['scope'] = $this->scope; + } + + return $this->composeUrl($this->authUrl, array_merge($defaultParams, $params)); + } + + /** + * Fetches access token from authorization code. + * @param string $authCode authorization code, usually comes at $_GET['code']. + * @param array $params additional request params. + * @return OAuthToken access token. + */ + public function fetchAccessToken($authCode, array $params = []) + { + $defaultParams = [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'code' => $authCode, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->getReturnUrl(), + ]; + $response = $this->sendRequest('POST', $this->tokenUrl, array_merge($defaultParams, $params)); + $token = $this->createToken(['params' => $response]); + $this->setAccessToken($token); + + return $token; + } + + /** + * Composes HTTP request CUrl options, which will be merged with the default ones. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array CUrl options. + * @throws Exception on failure. + */ + protected function composeRequestCurlOptions($method, $url, array $params) + { + $curlOptions = []; + switch ($method) { + case 'GET': { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + break; + } + case 'POST': { + $curlOptions[CURLOPT_POST] = true; + $curlOptions[CURLOPT_HTTPHEADER] = ['Content-type: application/x-www-form-urlencoded']; + $curlOptions[CURLOPT_POSTFIELDS] = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + break; + } + case 'HEAD': + case 'PUT': + case 'DELETE': { + $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; + if (!empty($params)) { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + } + break; + } + default: { + throw new Exception("Unknown request method '{$method}'."); + } + } + + return $curlOptions; + } + + /** + * Performs request to the OAuth API. + * @param OAuthToken $accessToken actual access token. + * @param string $url absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response. + * @throws Exception on failure. + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + $params['access_token'] = $accessToken->getToken(); + + return $this->sendRequest($method, $url, $params); + } + + /** + * Gets new auth token to replace expired one. + * @param OAuthToken $token expired auth token. + * @return OAuthToken new auth token. + */ + public function refreshAccessToken(OAuthToken $token) + { + $params = [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'refresh_token' + ]; + $params = array_merge($token->getParams(), $params); + $response = $this->sendRequest('POST', $this->tokenUrl, $params); + + return $response; + } + + /** + * Composes default {@link returnUrl} value. + * @return string return URL. + */ + protected function defaultReturnUrl() + { + $params = $_GET; + unset($params['code']); + $params[0] = Yii::$app->controller->getRoute(); + + return Yii::$app->getUrlManager()->createAbsoluteUrl($params); + } + + /** + * Creates token from its configuration. + * @param array $tokenConfig token configuration. + * @return OAuthToken token instance. + */ + protected function createToken(array $tokenConfig = []) + { + $tokenConfig['tokenParamKey'] = 'access_token'; + + return parent::createToken($tokenConfig); + } } diff --git a/extensions/authclient/OAuthToken.php b/extensions/authclient/OAuthToken.php index 9aac5b7d58e..db13dd9e105 100644 --- a/extensions/authclient/OAuthToken.php +++ b/extensions/authclient/OAuthToken.php @@ -26,181 +26,185 @@ */ class OAuthToken extends Object { - /** - * @var string key in {@link _params} array, which stores token key. - */ - public $tokenParamKey = 'oauth_token'; - /** - * @var string key in {@link _params} array, which stores token secret key. - */ - public $tokenSecretParamKey = 'oauth_token_secret'; - /** - * @var string key in {@link _params} array, which stores token expiration duration. - * If not set will attempt to fetch its value automatically. - */ - private $_expireDurationParamKey; - /** - * @var array token parameters. - */ - private $_params = []; - /** - * @var integer object creation timestamp. - */ - public $createTimestamp; - - public function init() - { - if ($this->createTimestamp === null) { - $this->createTimestamp = time(); - } - } - - /** - * @param string $expireDurationParamKey expire duration param key. - */ - public function setExpireDurationParamKey($expireDurationParamKey) - { - $this->_expireDurationParamKey = $expireDurationParamKey; - } - - /** - * @return string expire duration param key. - */ - public function getExpireDurationParamKey() - { - if ($this->_expireDurationParamKey === null) { - $this->_expireDurationParamKey = $this->defaultExpireDurationParamKey(); - } - return $this->_expireDurationParamKey; - } - - /** - * @return array - */ - public function getParams() - { - return $this->_params; - } - - /** - * @param array $params - */ - public function setParams(array $params) - { - $this->_params = $params; - } - - /** - * Sets param by name. - * @param string $name param name. - * @param mixed $value param value, - */ - public function setParam($name, $value) - { - $this->_params[$name] = $value; - } - - /** - * Returns param by name. - * @param string $name param name. - * @return mixed param value. - */ - public function getParam($name) - { - return isset($this->_params[$name]) ? $this->_params[$name] : null; - } - - /** - * Sets token value. - * @param string $token token value. - * @return static self reference. - */ - public function setToken($token) - { - $this->setParam($this->tokenParamKey, $token); - } - - /** - * Returns token value. - * @return string token value. - */ - public function getToken() - { - return $this->getParam($this->tokenParamKey); - } - - /** - * Sets the token secret value. - * @param string $tokenSecret token secret. - */ - public function setTokenSecret($tokenSecret) - { - $this->setParam($this->tokenSecretParamKey, $tokenSecret); - } - - /** - * Returns the token secret value. - * @return string token secret value. - */ - public function getTokenSecret() - { - return $this->getParam($this->tokenSecretParamKey); - } - - /** - * Sets token expire duration. - * @param string $expireDuration token expiration duration. - */ - public function setExpireDuration($expireDuration) - { - $this->setParam($this->getExpireDurationParamKey(), $expireDuration); - } - - /** - * Returns the token expiration duration. - * @return integer token expiration duration. - */ - public function getExpireDuration() - { - return $this->getParam($this->getExpireDurationParamKey()); - } - - /** - * Fetches default expire duration param key. - * @return string expire duration param key. - */ - protected function defaultExpireDurationParamKey() - { - $expireDurationParamKey = 'expires_in'; - foreach ($this->getParams() as $name => $value) { - if (strpos($name, 'expir') !== false) { - $expireDurationParamKey = $name; - break; - } - } - return $expireDurationParamKey; - } - - /** - * Checks if token has expired. - * @return boolean is token expired. - */ - public function getIsExpired() - { - $expirationDuration = $this->getExpireDuration(); - if (empty($expirationDuration)) { - return false; - } - return (time() >= ($this->createTimestamp + $expirationDuration)); - } - - /** - * Checks if token is valid. - * @return boolean is token valid. - */ - public function getIsValid() - { - $token = $this->getToken(); - return (!empty($token) && !$this->getIsExpired()); - } + /** + * @var string key in {@link _params} array, which stores token key. + */ + public $tokenParamKey = 'oauth_token'; + /** + * @var string key in {@link _params} array, which stores token secret key. + */ + public $tokenSecretParamKey = 'oauth_token_secret'; + /** + * @var string key in {@link _params} array, which stores token expiration duration. + * If not set will attempt to fetch its value automatically. + */ + private $_expireDurationParamKey; + /** + * @var array token parameters. + */ + private $_params = []; + /** + * @var integer object creation timestamp. + */ + public $createTimestamp; + + public function init() + { + if ($this->createTimestamp === null) { + $this->createTimestamp = time(); + } + } + + /** + * @param string $expireDurationParamKey expire duration param key. + */ + public function setExpireDurationParamKey($expireDurationParamKey) + { + $this->_expireDurationParamKey = $expireDurationParamKey; + } + + /** + * @return string expire duration param key. + */ + public function getExpireDurationParamKey() + { + if ($this->_expireDurationParamKey === null) { + $this->_expireDurationParamKey = $this->defaultExpireDurationParamKey(); + } + + return $this->_expireDurationParamKey; + } + + /** + * @return array + */ + public function getParams() + { + return $this->_params; + } + + /** + * @param array $params + */ + public function setParams(array $params) + { + $this->_params = $params; + } + + /** + * Sets param by name. + * @param string $name param name. + * @param mixed $value param value, + */ + public function setParam($name, $value) + { + $this->_params[$name] = $value; + } + + /** + * Returns param by name. + * @param string $name param name. + * @return mixed param value. + */ + public function getParam($name) + { + return isset($this->_params[$name]) ? $this->_params[$name] : null; + } + + /** + * Sets token value. + * @param string $token token value. + * @return static self reference. + */ + public function setToken($token) + { + $this->setParam($this->tokenParamKey, $token); + } + + /** + * Returns token value. + * @return string token value. + */ + public function getToken() + { + return $this->getParam($this->tokenParamKey); + } + + /** + * Sets the token secret value. + * @param string $tokenSecret token secret. + */ + public function setTokenSecret($tokenSecret) + { + $this->setParam($this->tokenSecretParamKey, $tokenSecret); + } + + /** + * Returns the token secret value. + * @return string token secret value. + */ + public function getTokenSecret() + { + return $this->getParam($this->tokenSecretParamKey); + } + + /** + * Sets token expire duration. + * @param string $expireDuration token expiration duration. + */ + public function setExpireDuration($expireDuration) + { + $this->setParam($this->getExpireDurationParamKey(), $expireDuration); + } + + /** + * Returns the token expiration duration. + * @return integer token expiration duration. + */ + public function getExpireDuration() + { + return $this->getParam($this->getExpireDurationParamKey()); + } + + /** + * Fetches default expire duration param key. + * @return string expire duration param key. + */ + protected function defaultExpireDurationParamKey() + { + $expireDurationParamKey = 'expires_in'; + foreach ($this->getParams() as $name => $value) { + if (strpos($name, 'expir') !== false) { + $expireDurationParamKey = $name; + break; + } + } + + return $expireDurationParamKey; + } + + /** + * Checks if token has expired. + * @return boolean is token expired. + */ + public function getIsExpired() + { + $expirationDuration = $this->getExpireDuration(); + if (empty($expirationDuration)) { + return false; + } + + return (time() >= ($this->createTimestamp + $expirationDuration)); + } + + /** + * Checks if token is valid. + * @return boolean is token valid. + */ + public function getIsValid() + { + $token = $this->getToken(); + + return (!empty($token) && !$this->getIsExpired()); + } } diff --git a/extensions/authclient/OpenId.php b/extensions/authclient/OpenId.php index 9e0dd38fa13..4dc134d2269 100644 --- a/extensions/authclient/OpenId.php +++ b/extensions/authclient/OpenId.php @@ -45,885 +45,908 @@ */ class OpenId extends BaseClient implements ClientInterface { - /** - * @var string authentication base URL, which should be used to compose actual authentication URL - * by [[buildAuthUrl()]] method. - */ - public $authUrl; - /** - * @var array list of attributes, which always should be returned from server. - * Attribute names should be always specified in AX format. - * For example: - * ~~~ - * ['namePerson/friendly', 'contact/email'] - * ~~~ - */ - public $requiredAttributes = []; - /** - * @var array list of attributes, which could be returned from server. - * Attribute names should be always specified in AX format. - * For example: - * ~~~ - * ['namePerson/first', 'namePerson/last'] - * ~~~ - */ - public $optionalAttributes = []; - - /** - * @var boolean whether to verify the peer's certificate. - */ - public $verifyPeer; - /** - * @var string directory that holds multiple CA certificates. - * This value will take effect only if [[verifyPeer]] is set. - */ - public $capath; - /** - * @var string the name of a file holding one or more certificates to verify the peer with. - * This value will take effect only if [[verifyPeer]] is set. - */ - public $cainfo; - - /** - * @var string authentication return URL. - */ - private $_returnUrl; - /** - * @var string claimed identifier (identity) - */ - private $_claimedId; - /** - * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. - */ - private $_trustRoot; - /** - * @var array data, which should be used to retrieve the OpenID response. - * If not set combination of GET and POST will be used. - */ - public $data; - /** - * @var array map of matches between AX and SREG attribute names in format: axAttributeName => sregAttributeName - */ - public $axToSregMap = [ - 'namePerson/friendly' => 'nickname', - 'contact/email' => 'email', - 'namePerson' => 'fullname', - 'birthDate' => 'dob', - 'person/gender' => 'gender', - 'contact/postalCode/home' => 'postcode', - 'contact/country/home' => 'country', - 'pref/language' => 'language', - 'pref/timezone' => 'timezone', - ]; - - /** - * @inheritdoc - */ - public function init() - { - if ($this->data === null) { - $this->data = array_merge($_GET, $_POST); // OPs may send data as POST or GET. - } - } - - /** - * @param string $claimedId claimed identifier (identity). - */ - public function setClaimedId($claimedId) - { - $this->_claimedId = $claimedId; - } - - /** - * @return string claimed identifier (identity). - */ - public function getClaimedId() - { - if ($this->_claimedId === null) { - if (isset($this->data['openid_claimed_id'])) { - $this->_claimedId = $this->data['openid_claimed_id']; - } elseif (isset($this->data['openid_identity'])) { - $this->_claimedId = $this->data['openid_identity']; - } - } - return $this->_claimedId; - } - - /** - * @param string $returnUrl authentication return URL. - */ - public function setReturnUrl($returnUrl) - { - $this->_returnUrl = $returnUrl; - } - - /** - * @return string authentication return URL. - */ - public function getReturnUrl() - { - if ($this->_returnUrl === null) { - $this->_returnUrl = $this->defaultReturnUrl(); - } - return $this->_returnUrl; - } - - /** - * @param string $value client trust root (realm). - */ - public function setTrustRoot($value) - { - $this->_trustRoot = $value; - } - - /** - * @return string client trust root (realm). - */ - public function getTrustRoot() - { - if ($this->_trustRoot === null) { - $this->_trustRoot = Yii::$app->getRequest()->getHostInfo(); - } - return $this->_trustRoot; - } - - /** - * Generates default [[returnUrl]] value. - * @return string default authentication return URL. - */ - protected function defaultReturnUrl() - { - $params = $_GET; - foreach ($params as $name => $value) { - if (strncmp('openid', $name, 6) === 0) { - unset($params[$name]); - } - } - $params[0] = Yii::$app->requestedRoute; - $url = Yii::$app->getUrlManager()->createUrl($params); - return $this->getTrustRoot() . $url; - } - - /** - * Checks if the server specified in the url exists. - * @param string $url URL to check - * @return boolean true, if the server exists; false otherwise - */ - public function hostExists($url) - { - if (strpos($url, '/') === false) { - $server = $url; - } else { - $server = @parse_url($url, PHP_URL_HOST); - } - if (!$server) { - return false; - } - $ips = gethostbynamel($server); - return !empty($ips); - } - - /** - * Sends HTTP request. - * @param string $url request URL. - * @param string $method request method. - * @param array $params request params. - * @return array|string response. - * @throws \yii\base\Exception on failure. - */ - protected function sendCurlRequest($url, $method = 'GET', $params = []) - { - $params = http_build_query($params, '', '&'); - $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($curl, CURLOPT_HEADER, false); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_HTTPHEADER, ['Accept: application/xrds+xml, */*']); - - if ($this->verifyPeer !== null) { - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer); - if ($this->capath) { - curl_setopt($curl, CURLOPT_CAPATH, $this->capath); - } - if ($this->cainfo) { - curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); - } - } - - if ($method == 'POST') { - curl_setopt($curl, CURLOPT_POST, true); - curl_setopt($curl, CURLOPT_POSTFIELDS, $params); - } elseif ($method == 'HEAD') { - curl_setopt($curl, CURLOPT_HEADER, true); - curl_setopt($curl, CURLOPT_NOBODY, true); - } else { - curl_setopt($curl, CURLOPT_HTTPGET, true); - } - $response = curl_exec($curl); - - if ($method == 'HEAD') { - $headers = []; - foreach (explode("\n", $response) as $header) { - $pos = strpos($header, ':'); - $name = strtolower(trim(substr($header, 0, $pos))); - $headers[$name] = trim(substr($header, $pos+1)); - } - return $headers; - } - - if (curl_errno($curl)) { - throw new Exception(curl_error($curl), curl_errno($curl)); - } - - return $response; - } - - /** - * Sends HTTP request. - * @param string $url request URL. - * @param string $method request method. - * @param array $params request params. - * @return array|string response. - * @throws \yii\base\Exception on failure. - * @throws \yii\base\NotSupportedException if request method is not supported. - */ - protected function sendStreamRequest($url, $method = 'GET', $params = []) - { - if (!$this->hostExists($url)) { - throw new Exception('Invalid request.'); - } - - $params = http_build_query($params, '', '&'); - switch ($method) { - case 'GET': - $options = [ - 'http' => [ - 'method' => 'GET', - 'header' => 'Accept: application/xrds+xml, */*', - 'ignore_errors' => true, - ] - ]; - $url = $url . ($params ? '?' . $params : ''); - break; - case 'POST': - $options = [ - 'http' => [ - 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => $params, - 'ignore_errors' => true, - ] - ]; - break; - case 'HEAD': - /* We want to send a HEAD request, - but since get_headers doesn't accept $context parameter, - we have to change the defaults.*/ - $default = stream_context_get_options(stream_context_get_default()); - stream_context_get_default([ - 'http' => [ - 'method' => 'HEAD', - 'header' => 'Accept: application/xrds+xml, */*', - 'ignore_errors' => true, - ] - ]); - - $url = $url . ($params ? '?' . $params : ''); - $headersTmp = get_headers($url); - if (empty($headersTmp)) { - return []; - } - - // Parsing headers. - $headers = []; - foreach ($headersTmp as $header) { - $pos = strpos($header, ':'); - $name = strtolower(trim(substr($header, 0, $pos))); - $headers[$name] = trim(substr($header, $pos + 1)); - } - - // and restore them - stream_context_get_default($default); - return $headers; - default: - throw new NotSupportedException("Method {$method} not supported"); - } - - if ($this->verifyPeer) { - $options = array_merge( - $options, - [ - 'ssl' => [ - 'verify_peer' => true, - 'capath' => $this->capath, - 'cafile' => $this->cainfo, - ] - ] - ); - } - - $context = stream_context_create($options); - return file_get_contents($url, false, $context); - } - - /** - * Sends request to the server - * @param string $url request URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array|string response. - */ - protected function sendRequest($url, $method = 'GET', $params = []) - { - if (function_exists('curl_init') && !ini_get('safe_mode')) { - return $this->sendCurlRequest($url, $method, $params); - } - return $this->sendStreamRequest($url, $method, $params); - } - - /** - * Combines given URLs into single one. - * @param string $baseUrl base URL. - * @param string|array $additionalUrl additional URL string or information array. - * @return string composed URL. - */ - protected function buildUrl($baseUrl, $additionalUrl) - { - $baseUrl = parse_url($baseUrl); - if (!is_array($additionalUrl)) { - $additionalUrl = parse_url($additionalUrl); - } - - if (isset($baseUrl['query'], $additionalUrl['query'])) { - $additionalUrl['query'] = $baseUrl['query'] . '&' . $additionalUrl['query']; - } - - $urlInfo = array_merge($baseUrl, $additionalUrl); - $url = $urlInfo['scheme'] . '://' - . (empty($urlInfo['username']) ? '' - :(empty($urlInfo['password']) ? "{$urlInfo['username']}@" - :"{$urlInfo['username']}:{$urlInfo['password']}@")) - . $urlInfo['host'] - . (empty($urlInfo['port']) ? '' : ":{$urlInfo['port']}") - . (empty($urlInfo['path']) ? '' : $urlInfo['path']) - . (empty($urlInfo['query']) ? '' : "?{$urlInfo['query']}") - . (empty($urlInfo['fragment']) ? '' : "#{$urlInfo['fragment']}"); - return $url; - } - - /** - * Scans content for / tags and extract information from them. - * @param string $content HTML content to be be parsed. - * @param string $tag name of the source tag. - * @param string $matchAttributeName name of the source tag attribute, which should contain $matchAttributeValue - * @param string $matchAttributeValue required value of $matchAttributeName - * @param string $valueAttributeName name of the source tag attribute, which should contain searched value. - * @return string|boolean searched value, "false" on failure. - */ - protected function extractHtmlTagValue($content, $tag, $matchAttributeName, $matchAttributeValue, $valueAttributeName) - { - preg_match_all("#<{$tag}[^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*$valueAttributeName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); - preg_match_all("#<{$tag}[^>]*$valueAttributeName=['\"](.+?)['\"][^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*/?>#i", $content, $matches2); - $result = array_merge($matches1[1], $matches2[1]); - return empty($result) ? false : $result[0]; - } - - /** - * Performs Yadis and HTML discovery. - * @param string $url Identity URL. - * @return array OpenID provider info, following keys will be available: - * - 'url' - string OP Endpoint (i.e. OpenID provider address). - * - 'version' - integer OpenID protocol version used by provider. - * - 'identity' - string identity value. - * - 'identifier_select' - boolean whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. - * - 'ax' - boolean whether AX attributes should be used. - * - 'sreg' - boolean whether SREG attributes should be used. - * @throws Exception on failure. - */ - public function discover($url) - { - if (empty($url)) { - throw new Exception('No identity supplied.'); - } - $result = [ - 'url' => null, - 'version' => null, - 'identity' => $url, - 'identifier_select' => false, - 'ax' => false, - 'sreg' => false, - ]; - - // Use xri.net proxy to resolve i-name identities - if (!preg_match('#^https?:#', $url)) { - $url = 'https://xri.net/' . $url; - } - - /* We save the original url in case of Yadis discovery failure. - It can happen when we'll be lead to an XRDS document - which does not have any OpenID2 services.*/ - $originalUrl = $url; - - // A flag to disable yadis discovery in case of failure in headers. - $yadis = true; - - // We'll jump a maximum of 5 times, to avoid endless redirections. - for ($i = 0; $i < 5; $i ++) { - if ($yadis) { - $headers = $this->sendRequest($url, 'HEAD'); - - $next = false; - if (isset($headers['x-xrds-location'])) { - $url = $this->buildUrl($url, trim($headers['x-xrds-location'])); - $next = true; - } - - if (isset($headers['content-type']) - && (strpos($headers['content-type'], 'application/xrds+xml') !== false - || strpos($headers['content-type'], 'text/xml') !== false) - ) { - /* Apparently, some providers return XRDS documents as text/html. - While it is against the spec, allowing this here shouldn't break - compatibility with anything. - --- - Found an XRDS document, now let's find the server, and optionally delegate.*/ - $content = $this->sendRequest($url, 'GET'); - - preg_match_all('#(.*?)#s', $content, $m); - foreach ($m[1] as $content) { - $content = ' ' . $content; // The space is added, so that strpos doesn't return 0. - - // OpenID 2 - $ns = preg_quote('http://specs.openid.net/auth/2.0/'); - if (preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { - if ($type[1] == 'server') { - $result['identifier_select'] = true; - } - - preg_match('#(.*)#', $content, $server); - preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); - if (empty($server)) { - throw new Exception('No servers found!'); - } - // Does the server advertise support for either AX or SREG? - $result['ax'] = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); - $result['sreg'] = strpos($content, 'http://openid.net/sreg/1.0') || strpos($content, 'http://openid.net/extensions/sreg/1.1'); - - $server = $server[1]; - if (isset($delegate[2])) { - $result['identity'] = trim($delegate[2]); - } - - $result['url'] = $server; - $result['version'] = 2; - return $result; - } - - // OpenID 1.1 - $ns = preg_quote('http://openid.net/signon/1.1'); - if (preg_match('#\s*'.$ns.'\s*#s', $content)) { - preg_match('#(.*)#', $content, $server); - preg_match('#<.*?Delegate>(.*)#', $content, $delegate); - if (empty($server)) { - throw new Exception('No servers found!'); - } - // AX can be used only with OpenID 2.0, so checking only SREG - $result['sreg'] = strpos($content, 'http://openid.net/sreg/1.0') || strpos($content, 'http://openid.net/extensions/sreg/1.1'); - - $server = $server[1]; - if (isset($delegate[1])) { - $result['identity'] = $delegate[1]; - } - - $result['url'] = $server; - $result['version'] = 1; - return $result; - } - } - - $next = true; - $yadis = false; - $url = $originalUrl; - $content = null; - break; - } - if ($next) { - continue; - } - - // There are no relevant information in headers, so we search the body. - $content = $this->sendRequest($url, 'GET'); - $location = $this->extractHtmlTagValue($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); - if ($location) { - $url = $this->buildUrl($url, $location); - continue; - } - } - - if (!isset($content)) { - $content = $this->sendRequest($url, 'GET'); - } - - // At this point, the YADIS Discovery has failed, so we'll switch to openid2 HTML discovery, then fallback to openid 1.1 discovery. - $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.provider', 'href'); - if (!$server) { - // The same with openid 1.1 - $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); - $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.delegate', 'href'); - $version = 1; - } else { - $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.local_id', 'href'); - $version = 2; - } - - if ($server) { - // We found an OpenID2 OP Endpoint - if ($delegate) { - // We have also found an OP-Local ID. - $result['identity'] = $delegate; - } - $result['url'] = $server; - $result['version'] = $version; - return $result; - } - throw new Exception('No servers found!'); - } - throw new Exception('Endless redirection!'); - } - - /** - * Composes SREG request parameters. - * @return array SREG parameters. - */ - protected function buildSregParams() - { - $params = []; - /* We always use SREG 1.1, even if the server is advertising only support for 1.0. - That's because it's fully backwards compatible with 1.0, and some providers - advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com */ - $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; - if (!empty($this->requiredAttributes)) { - $params['openid.sreg.required'] = []; - foreach ($this->requiredAttributes as $required) { - if (!isset($this->axToSregMap[$required])) { - continue; - } - $params['openid.sreg.required'][] = $this->axToSregMap[$required]; - } - $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); - } - - if (!empty($this->optionalAttributes)) { - $params['openid.sreg.optional'] = []; - foreach ($this->optionalAttributes as $optional) { - if (!isset($this->axToSregMap[$optional])) { - continue; - } - $params['openid.sreg.optional'][] = $this->axToSregMap[$optional]; - } - $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); - } - return $params; - } - - /** - * Composes AX request parameters. - * @return array AX parameters. - */ - protected function buildAxParams() - { - $params = []; - if (!empty($this->requiredAttributes) || !empty($this->optionalAttributes)) { - $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; - $params['openid.ax.mode'] = 'fetch_request'; - $aliases = []; - $counts = []; - $requiredAttributes = []; - $optionalAttributes = []; - foreach (['requiredAttributes', 'optionalAttributes'] as $type) { - foreach ($this->$type as $alias => $field) { - if (is_int($alias)) { - $alias = strtr($field, '/', '_'); - } - $aliases[$alias] = 'http://axschema.org/' . $field; - if (empty($counts[$alias])) { - $counts[$alias] = 0; - } - $counts[$alias] += 1; - ${$type}[] = $alias; - } - } - foreach ($aliases as $alias => $ns) { - $params['openid.ax.type.' . $alias] = $ns; - } - foreach ($counts as $alias => $count) { - if ($count == 1) { - continue; - } - $params['openid.ax.count.' . $alias] = $count; - } - - // Don't send empty ax.required and ax.if_available. - // Google and possibly other providers refuse to support ax when one of these is empty. - if (!empty($requiredAttributes)) { - $params['openid.ax.required'] = implode(',', $requiredAttributes); - } - if (!empty($optionalAttributes)) { - $params['openid.ax.if_available'] = implode(',', $optionalAttributes); - } - } - return $params; - } - - /** - * Builds authentication URL for the protocol version 1. - * @param array $serverInfo OpenID server info. - * @return string authentication URL. - */ - protected function buildAuthUrlV1($serverInfo) - { - $returnUrl = $this->getReturnUrl(); - /* If we have an openid.delegate that is different from our claimed id, - we need to somehow preserve the claimed id between requests. - The simplest way is to just send it along with the return_to url.*/ - if ($serverInfo['identity'] != $this->getClaimedId()) { - $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->getClaimedId(); - } - - $params = array_merge( - [ - 'openid.return_to' => $returnUrl, - 'openid.mode' => 'checkid_setup', - 'openid.identity' => $serverInfo['identity'], - 'openid.trust_root' => $this->trustRoot, - ], - $this->buildSregParams() - ); - - return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); - } - - /** - * Builds authentication URL for the protocol version 2. - * @param array $serverInfo OpenID server info. - * @return string authentication URL. - */ - protected function buildAuthUrlV2($serverInfo) - { - $params = [ - 'openid.ns' => 'http://specs.openid.net/auth/2.0', - 'openid.mode' => 'checkid_setup', - 'openid.return_to' => $this->getReturnUrl(), - 'openid.realm' => $this->getTrustRoot(), - ]; - if ($serverInfo['ax']) { - $params = array_merge($params, $this->buildAxParams()); - } - if ($serverInfo['sreg']) { - $params = array_merge($params, $this->buildSregParams()); - } - if (!$serverInfo['ax'] && !$serverInfo['sreg']) { - // If OP doesn't advertise either SREG, nor AX, let's send them both in worst case we don't get anything in return. - $params = array_merge($this->buildSregParams(), $this->buildAxParams(), $params); - } - - if ($serverInfo['identifier_select']) { - $url = 'http://specs.openid.net/auth/2.0/identifier_select'; - $params['openid.identity'] = $url; - $params['openid.claimed_id']= $url; - } else { - $params['openid.identity'] = $serverInfo['identity']; - $params['openid.claimed_id'] = $this->getClaimedId(); - } - return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); - } - - /** - * Returns authentication URL. Usually, you want to redirect your user to it. - * @param boolean $identifierSelect whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. - * @return string the authentication URL. - * @throws Exception on failure. - */ - public function buildAuthUrl($identifierSelect = null) - { - $authUrl = $this->authUrl; - $claimedId = $this->getClaimedId(); - if (empty($claimedId)) { - $this->setClaimedId($authUrl); - } - $serverInfo = $this->discover($authUrl); - if ($serverInfo['version'] == 2) { - if ($identifierSelect !== null) { - $serverInfo['identifier_select'] = $identifierSelect; - } - return $this->buildAuthUrlV2($serverInfo); - } - return $this->buildAuthUrlV1($serverInfo); - } - - /** - * Performs OpenID verification with the OP. - * @param boolean $validateRequiredAttributes whether to validate required attributes. - * @return boolean whether the verification was successful. - */ - public function validate($validateRequiredAttributes = true) - { - $claimedId = $this->getClaimedId(); - if (empty($claimedId)) { - return false; - } - $params = [ - 'openid.assoc_handle' => $this->data['openid_assoc_handle'], - 'openid.signed' => $this->data['openid_signed'], - 'openid.sig' => $this->data['openid_sig'], - ]; - - if (isset($this->data['openid_ns'])) { - /* We're dealing with an OpenID 2.0 server, so let's set an ns - Even though we should know location of the endpoint, - we still need to verify it by discovery, so $server is not set here*/ - $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; - } elseif (isset($this->data['openid_claimed_id']) && $this->data['openid_claimed_id'] != $this->data['openid_identity']) { - // If it's an OpenID 1 provider, and we've got claimed_id, - // we have to append it to the returnUrl, like authUrlV1 does. - $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $claimedId; - } - - if ($this->data['openid_return_to'] != $this->returnUrl) { - // The return_to url must match the url of current request. - return false; - } - - $serverInfo = $this->discover($claimedId); - - foreach (explode(',', $this->data['openid_signed']) as $item) { - $value = $this->data['openid_' . str_replace('.', '_', $item)]; - $params['openid.' . $item] = $value; - } - - $params['openid.mode'] = 'check_authentication'; - - $response = $this->sendRequest($serverInfo['url'], 'POST', $params); - - if (preg_match('/is_valid\s*:\s*true/i', $response)) { - if ($validateRequiredAttributes) { - return $this->validateRequiredAttributes(); - } else { - return true; - } - } else { - return false; - } - } - - /** - * Checks if all required attributes are present in the server response. - * @return boolean whether all required attributes are present. - */ - protected function validateRequiredAttributes() - { - if (!empty($this->requiredAttributes)) { - $attributes = $this->fetchAttributes(); - foreach ($this->requiredAttributes as $openIdAttributeName) { - if (!isset($attributes[$openIdAttributeName])) { - return false; - } - } - } - return true; - } - - /** - * Gets AX attributes provided by OP. - * @return array array of attributes. - */ - protected function fetchAxAttributes() - { - $alias = null; - if (isset($this->data['openid_ns_ax']) && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0') { - // It's the most likely case, so we'll check it before - $alias = 'ax'; - } else { - // 'ax' prefix is either undefined, or points to another extension, so we search for another prefix - foreach ($this->data as $key => $value) { - if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_' && $value == 'http://openid.net/srv/ax/1.0') { - $alias = substr($key, strlen('openid_ns_')); - break; - } - } - } - if (!$alias) { - // An alias for AX schema has not been found, so there is no AX data in the OP's response - return []; - } - - $attributes = []; - foreach ($this->data as $key => $value) { - $keyMatch = 'openid_' . $alias . '_value_'; - if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { - continue; - } - $key = substr($key, strlen($keyMatch)); - if (!isset($this->data['openid_' . $alias . '_type_' . $key])) { - /* OP is breaking the spec by returning a field without - associated ns. This shouldn't happen, but it's better - to check, than cause an E_NOTICE.*/ - continue; - } - $key = substr($this->data['openid_' . $alias . '_type_' . $key], strlen('http://axschema.org/')); - $attributes[$key] = $value; - } - return $attributes; - } - - /** - * Gets SREG attributes provided by OP. SREG names will be mapped to AX names. - * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' - */ - protected function fetchSregAttributes() - { - $attributes = []; - $sregToAx = array_flip($this->axToSregMap); - foreach ($this->data as $key => $value) { - $keyMatch = 'openid_sreg_'; - if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { - continue; - } - $key = substr($key, strlen($keyMatch)); - if (!isset($sregToAx[$key])) { - // The field name isn't part of the SREG spec, so we ignore it. - continue; - } - $attributes[$sregToAx[$key]] = $value; - } - return $attributes; - } - - /** - * Gets AX/SREG attributes provided by OP. Should be used only after successful validation. - * Note that it does not guarantee that any of the required/optional parameters will be present, - * or that there will be no other attributes besides those specified. - * In other words. OP may provide whatever information it wants to. - * SREG names will be mapped to AX names. - * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' - * @see http://www.axschema.org/types/ - */ - public function fetchAttributes() - { - if (isset($this->data['openid_ns']) && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0') { - // OpenID 2.0 - // We search for both AX and SREG attributes, with AX taking precedence. - return array_merge($this->fetchSregAttributes(), $this->fetchAxAttributes()); - } - return $this->fetchSregAttributes(); - } - - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - return array_merge(['id' => $this->getClaimedId()], $this->fetchAttributes()); - } + /** + * @var string authentication base URL, which should be used to compose actual authentication URL + * by [[buildAuthUrl()]] method. + */ + public $authUrl; + /** + * @var array list of attributes, which always should be returned from server. + * Attribute names should be always specified in AX format. + * For example: + * ~~~ + * ['namePerson/friendly', 'contact/email'] + * ~~~ + */ + public $requiredAttributes = []; + /** + * @var array list of attributes, which could be returned from server. + * Attribute names should be always specified in AX format. + * For example: + * ~~~ + * ['namePerson/first', 'namePerson/last'] + * ~~~ + */ + public $optionalAttributes = []; + + /** + * @var boolean whether to verify the peer's certificate. + */ + public $verifyPeer; + /** + * @var string directory that holds multiple CA certificates. + * This value will take effect only if [[verifyPeer]] is set. + */ + public $capath; + /** + * @var string the name of a file holding one or more certificates to verify the peer with. + * This value will take effect only if [[verifyPeer]] is set. + */ + public $cainfo; + + /** + * @var string authentication return URL. + */ + private $_returnUrl; + /** + * @var string claimed identifier (identity) + */ + private $_claimedId; + /** + * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. + */ + private $_trustRoot; + /** + * @var array data, which should be used to retrieve the OpenID response. + * If not set combination of GET and POST will be used. + */ + public $data; + /** + * @var array map of matches between AX and SREG attribute names in format: axAttributeName => sregAttributeName + */ + public $axToSregMap = [ + 'namePerson/friendly' => 'nickname', + 'contact/email' => 'email', + 'namePerson' => 'fullname', + 'birthDate' => 'dob', + 'person/gender' => 'gender', + 'contact/postalCode/home' => 'postcode', + 'contact/country/home' => 'country', + 'pref/language' => 'language', + 'pref/timezone' => 'timezone', + ]; + + /** + * @inheritdoc + */ + public function init() + { + if ($this->data === null) { + $this->data = array_merge($_GET, $_POST); // OPs may send data as POST or GET. + } + } + + /** + * @param string $claimedId claimed identifier (identity). + */ + public function setClaimedId($claimedId) + { + $this->_claimedId = $claimedId; + } + + /** + * @return string claimed identifier (identity). + */ + public function getClaimedId() + { + if ($this->_claimedId === null) { + if (isset($this->data['openid_claimed_id'])) { + $this->_claimedId = $this->data['openid_claimed_id']; + } elseif (isset($this->data['openid_identity'])) { + $this->_claimedId = $this->data['openid_identity']; + } + } + + return $this->_claimedId; + } + + /** + * @param string $returnUrl authentication return URL. + */ + public function setReturnUrl($returnUrl) + { + $this->_returnUrl = $returnUrl; + } + + /** + * @return string authentication return URL. + */ + public function getReturnUrl() + { + if ($this->_returnUrl === null) { + $this->_returnUrl = $this->defaultReturnUrl(); + } + + return $this->_returnUrl; + } + + /** + * @param string $value client trust root (realm). + */ + public function setTrustRoot($value) + { + $this->_trustRoot = $value; + } + + /** + * @return string client trust root (realm). + */ + public function getTrustRoot() + { + if ($this->_trustRoot === null) { + $this->_trustRoot = Yii::$app->getRequest()->getHostInfo(); + } + + return $this->_trustRoot; + } + + /** + * Generates default [[returnUrl]] value. + * @return string default authentication return URL. + */ + protected function defaultReturnUrl() + { + $params = $_GET; + foreach ($params as $name => $value) { + if (strncmp('openid', $name, 6) === 0) { + unset($params[$name]); + } + } + $params[0] = Yii::$app->requestedRoute; + $url = Yii::$app->getUrlManager()->createUrl($params); + + return $this->getTrustRoot() . $url; + } + + /** + * Checks if the server specified in the url exists. + * @param string $url URL to check + * @return boolean true, if the server exists; false otherwise + */ + public function hostExists($url) + { + if (strpos($url, '/') === false) { + $server = $url; + } else { + $server = @parse_url($url, PHP_URL_HOST); + } + if (!$server) { + return false; + } + $ips = gethostbynamel($server); + + return !empty($ips); + } + + /** + * Sends HTTP request. + * @param string $url request URL. + * @param string $method request method. + * @param array $params request params. + * @return array|string response. + * @throws \yii\base\Exception on failure. + */ + protected function sendCurlRequest($url, $method = 'GET', $params = []) + { + $params = http_build_query($params, '', '&'); + $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, ['Accept: application/xrds+xml, */*']); + + if ($this->verifyPeer !== null) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer); + if ($this->capath) { + curl_setopt($curl, CURLOPT_CAPATH, $this->capath); + } + if ($this->cainfo) { + curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); + } + } + + if ($method == 'POST') { + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $params); + } elseif ($method == 'HEAD') { + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, true); + } else { + curl_setopt($curl, CURLOPT_HTTPGET, true); + } + $response = curl_exec($curl); + + if ($method == 'HEAD') { + $headers = []; + foreach (explode("\n", $response) as $header) { + $pos = strpos($header, ':'); + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + } + + return $headers; + } + + if (curl_errno($curl)) { + throw new Exception(curl_error($curl), curl_errno($curl)); + } + + return $response; + } + + /** + * Sends HTTP request. + * @param string $url request URL. + * @param string $method request method. + * @param array $params request params. + * @return array|string response. + * @throws \yii\base\Exception on failure. + * @throws \yii\base\NotSupportedException if request method is not supported. + */ + protected function sendStreamRequest($url, $method = 'GET', $params = []) + { + if (!$this->hostExists($url)) { + throw new Exception('Invalid request.'); + } + + $params = http_build_query($params, '', '&'); + switch ($method) { + case 'GET': + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => 'Accept: application/xrds+xml, */*', + 'ignore_errors' => true, + ] + ]; + $url = $url . ($params ? '?' . $params : ''); + break; + case 'POST': + $options = [ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $params, + 'ignore_errors' => true, + ] + ]; + break; + case 'HEAD': + /* We want to send a HEAD request, + but since get_headers doesn't accept $context parameter, + we have to change the defaults.*/ + $default = stream_context_get_options(stream_context_get_default()); + stream_context_get_default([ + 'http' => [ + 'method' => 'HEAD', + 'header' => 'Accept: application/xrds+xml, */*', + 'ignore_errors' => true, + ] + ]); + + $url = $url . ($params ? '?' . $params : ''); + $headersTmp = get_headers($url); + if (empty($headersTmp)) { + return []; + } + + // Parsing headers. + $headers = []; + foreach ($headersTmp as $header) { + $pos = strpos($header, ':'); + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos + 1)); + } + + // and restore them + stream_context_get_default($default); + + return $headers; + default: + throw new NotSupportedException("Method {$method} not supported"); + } + + if ($this->verifyPeer) { + $options = array_merge( + $options, + [ + 'ssl' => [ + 'verify_peer' => true, + 'capath' => $this->capath, + 'cafile' => $this->cainfo, + ] + ] + ); + } + + $context = stream_context_create($options); + + return file_get_contents($url, false, $context); + } + + /** + * Sends request to the server + * @param string $url request URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array|string response. + */ + protected function sendRequest($url, $method = 'GET', $params = []) + { + if (function_exists('curl_init') && !ini_get('safe_mode')) { + return $this->sendCurlRequest($url, $method, $params); + } + + return $this->sendStreamRequest($url, $method, $params); + } + + /** + * Combines given URLs into single one. + * @param string $baseUrl base URL. + * @param string|array $additionalUrl additional URL string or information array. + * @return string composed URL. + */ + protected function buildUrl($baseUrl, $additionalUrl) + { + $baseUrl = parse_url($baseUrl); + if (!is_array($additionalUrl)) { + $additionalUrl = parse_url($additionalUrl); + } + + if (isset($baseUrl['query'], $additionalUrl['query'])) { + $additionalUrl['query'] = $baseUrl['query'] . '&' . $additionalUrl['query']; + } + + $urlInfo = array_merge($baseUrl, $additionalUrl); + $url = $urlInfo['scheme'] . '://' + . (empty($urlInfo['username']) ? '' + :(empty($urlInfo['password']) ? "{$urlInfo['username']}@" + :"{$urlInfo['username']}:{$urlInfo['password']}@")) + . $urlInfo['host'] + . (empty($urlInfo['port']) ? '' : ":{$urlInfo['port']}") + . (empty($urlInfo['path']) ? '' : $urlInfo['path']) + . (empty($urlInfo['query']) ? '' : "?{$urlInfo['query']}") + . (empty($urlInfo['fragment']) ? '' : "#{$urlInfo['fragment']}"); + + return $url; + } + + /** + * Scans content for / tags and extract information from them. + * @param string $content HTML content to be be parsed. + * @param string $tag name of the source tag. + * @param string $matchAttributeName name of the source tag attribute, which should contain $matchAttributeValue + * @param string $matchAttributeValue required value of $matchAttributeName + * @param string $valueAttributeName name of the source tag attribute, which should contain searched value. + * @return string|boolean searched value, "false" on failure. + */ + protected function extractHtmlTagValue($content, $tag, $matchAttributeName, $matchAttributeValue, $valueAttributeName) + { + preg_match_all("#<{$tag}[^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*$valueAttributeName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); + preg_match_all("#<{$tag}[^>]*$valueAttributeName=['\"](.+?)['\"][^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*/?>#i", $content, $matches2); + $result = array_merge($matches1[1], $matches2[1]); + + return empty($result) ? false : $result[0]; + } + + /** + * Performs Yadis and HTML discovery. + * @param string $url Identity URL. + * @return array OpenID provider info, following keys will be available: + * - 'url' - string OP Endpoint (i.e. OpenID provider address). + * - 'version' - integer OpenID protocol version used by provider. + * - 'identity' - string identity value. + * - 'identifier_select' - boolean whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. + * - 'ax' - boolean whether AX attributes should be used. + * - 'sreg' - boolean whether SREG attributes should be used. + * @throws Exception on failure. + */ + public function discover($url) + { + if (empty($url)) { + throw new Exception('No identity supplied.'); + } + $result = [ + 'url' => null, + 'version' => null, + 'identity' => $url, + 'identifier_select' => false, + 'ax' => false, + 'sreg' => false, + ]; + + // Use xri.net proxy to resolve i-name identities + if (!preg_match('#^https?:#', $url)) { + $url = 'https://xri.net/' . $url; + } + + /* We save the original url in case of Yadis discovery failure. + It can happen when we'll be lead to an XRDS document + which does not have any OpenID2 services.*/ + $originalUrl = $url; + + // A flag to disable yadis discovery in case of failure in headers. + $yadis = true; + + // We'll jump a maximum of 5 times, to avoid endless redirections. + for ($i = 0; $i < 5; $i ++) { + if ($yadis) { + $headers = $this->sendRequest($url, 'HEAD'); + + $next = false; + if (isset($headers['x-xrds-location'])) { + $url = $this->buildUrl($url, trim($headers['x-xrds-location'])); + $next = true; + } + + if (isset($headers['content-type']) + && (strpos($headers['content-type'], 'application/xrds+xml') !== false + || strpos($headers['content-type'], 'text/xml') !== false) + ) { + /* Apparently, some providers return XRDS documents as text/html. + While it is against the spec, allowing this here shouldn't break + compatibility with anything. + --- + Found an XRDS document, now let's find the server, and optionally delegate.*/ + $content = $this->sendRequest($url, 'GET'); + + preg_match_all('#(.*?)#s', $content, $m); + foreach ($m[1] as $content) { + $content = ' ' . $content; // The space is added, so that strpos doesn't return 0. + + // OpenID 2 + $ns = preg_quote('http://specs.openid.net/auth/2.0/'); + if (preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { + if ($type[1] == 'server') { + $result['identifier_select'] = true; + } + + preg_match('#(.*)#', $content, $server); + preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); + if (empty($server)) { + throw new Exception('No servers found!'); + } + // Does the server advertise support for either AX or SREG? + $result['ax'] = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); + $result['sreg'] = strpos($content, 'http://openid.net/sreg/1.0') || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[2])) { + $result['identity'] = trim($delegate[2]); + } + + $result['url'] = $server; + $result['version'] = 2; + + return $result; + } + + // OpenID 1.1 + $ns = preg_quote('http://openid.net/signon/1.1'); + if (preg_match('#\s*'.$ns.'\s*#s', $content)) { + preg_match('#(.*)#', $content, $server); + preg_match('#<.*?Delegate>(.*)#', $content, $delegate); + if (empty($server)) { + throw new Exception('No servers found!'); + } + // AX can be used only with OpenID 2.0, so checking only SREG + $result['sreg'] = strpos($content, 'http://openid.net/sreg/1.0') || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[1])) { + $result['identity'] = $delegate[1]; + } + + $result['url'] = $server; + $result['version'] = 1; + + return $result; + } + } + + $next = true; + $yadis = false; + $url = $originalUrl; + $content = null; + break; + } + if ($next) { + continue; + } + + // There are no relevant information in headers, so we search the body. + $content = $this->sendRequest($url, 'GET'); + $location = $this->extractHtmlTagValue($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); + if ($location) { + $url = $this->buildUrl($url, $location); + continue; + } + } + + if (!isset($content)) { + $content = $this->sendRequest($url, 'GET'); + } + + // At this point, the YADIS Discovery has failed, so we'll switch to openid2 HTML discovery, then fallback to openid 1.1 discovery. + $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.provider', 'href'); + if (!$server) { + // The same with openid 1.1 + $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); + $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.delegate', 'href'); + $version = 1; + } else { + $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.local_id', 'href'); + $version = 2; + } + + if ($server) { + // We found an OpenID2 OP Endpoint + if ($delegate) { + // We have also found an OP-Local ID. + $result['identity'] = $delegate; + } + $result['url'] = $server; + $result['version'] = $version; + + return $result; + } + throw new Exception('No servers found!'); + } + throw new Exception('Endless redirection!'); + } + + /** + * Composes SREG request parameters. + * @return array SREG parameters. + */ + protected function buildSregParams() + { + $params = []; + /* We always use SREG 1.1, even if the server is advertising only support for 1.0. + That's because it's fully backwards compatible with 1.0, and some providers + advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com */ + $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; + if (!empty($this->requiredAttributes)) { + $params['openid.sreg.required'] = []; + foreach ($this->requiredAttributes as $required) { + if (!isset($this->axToSregMap[$required])) { + continue; + } + $params['openid.sreg.required'][] = $this->axToSregMap[$required]; + } + $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); + } + + if (!empty($this->optionalAttributes)) { + $params['openid.sreg.optional'] = []; + foreach ($this->optionalAttributes as $optional) { + if (!isset($this->axToSregMap[$optional])) { + continue; + } + $params['openid.sreg.optional'][] = $this->axToSregMap[$optional]; + } + $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); + } + + return $params; + } + + /** + * Composes AX request parameters. + * @return array AX parameters. + */ + protected function buildAxParams() + { + $params = []; + if (!empty($this->requiredAttributes) || !empty($this->optionalAttributes)) { + $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; + $params['openid.ax.mode'] = 'fetch_request'; + $aliases = []; + $counts = []; + $requiredAttributes = []; + $optionalAttributes = []; + foreach (['requiredAttributes', 'optionalAttributes'] as $type) { + foreach ($this->$type as $alias => $field) { + if (is_int($alias)) { + $alias = strtr($field, '/', '_'); + } + $aliases[$alias] = 'http://axschema.org/' . $field; + if (empty($counts[$alias])) { + $counts[$alias] = 0; + } + $counts[$alias] += 1; + ${$type}[] = $alias; + } + } + foreach ($aliases as $alias => $ns) { + $params['openid.ax.type.' . $alias] = $ns; + } + foreach ($counts as $alias => $count) { + if ($count == 1) { + continue; + } + $params['openid.ax.count.' . $alias] = $count; + } + + // Don't send empty ax.required and ax.if_available. + // Google and possibly other providers refuse to support ax when one of these is empty. + if (!empty($requiredAttributes)) { + $params['openid.ax.required'] = implode(',', $requiredAttributes); + } + if (!empty($optionalAttributes)) { + $params['openid.ax.if_available'] = implode(',', $optionalAttributes); + } + } + + return $params; + } + + /** + * Builds authentication URL for the protocol version 1. + * @param array $serverInfo OpenID server info. + * @return string authentication URL. + */ + protected function buildAuthUrlV1($serverInfo) + { + $returnUrl = $this->getReturnUrl(); + /* If we have an openid.delegate that is different from our claimed id, + we need to somehow preserve the claimed id between requests. + The simplest way is to just send it along with the return_to url.*/ + if ($serverInfo['identity'] != $this->getClaimedId()) { + $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->getClaimedId(); + } + + $params = array_merge( + [ + 'openid.return_to' => $returnUrl, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $serverInfo['identity'], + 'openid.trust_root' => $this->trustRoot, + ], + $this->buildSregParams() + ); + + return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); + } + + /** + * Builds authentication URL for the protocol version 2. + * @param array $serverInfo OpenID server info. + * @return string authentication URL. + */ + protected function buildAuthUrlV2($serverInfo) + { + $params = [ + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.mode' => 'checkid_setup', + 'openid.return_to' => $this->getReturnUrl(), + 'openid.realm' => $this->getTrustRoot(), + ]; + if ($serverInfo['ax']) { + $params = array_merge($params, $this->buildAxParams()); + } + if ($serverInfo['sreg']) { + $params = array_merge($params, $this->buildSregParams()); + } + if (!$serverInfo['ax'] && !$serverInfo['sreg']) { + // If OP doesn't advertise either SREG, nor AX, let's send them both in worst case we don't get anything in return. + $params = array_merge($this->buildSregParams(), $this->buildAxParams(), $params); + } + + if ($serverInfo['identifier_select']) { + $url = 'http://specs.openid.net/auth/2.0/identifier_select'; + $params['openid.identity'] = $url; + $params['openid.claimed_id']= $url; + } else { + $params['openid.identity'] = $serverInfo['identity']; + $params['openid.claimed_id'] = $this->getClaimedId(); + } + + return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); + } + + /** + * Returns authentication URL. Usually, you want to redirect your user to it. + * @param boolean $identifierSelect whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. + * @return string the authentication URL. + * @throws Exception on failure. + */ + public function buildAuthUrl($identifierSelect = null) + { + $authUrl = $this->authUrl; + $claimedId = $this->getClaimedId(); + if (empty($claimedId)) { + $this->setClaimedId($authUrl); + } + $serverInfo = $this->discover($authUrl); + if ($serverInfo['version'] == 2) { + if ($identifierSelect !== null) { + $serverInfo['identifier_select'] = $identifierSelect; + } + + return $this->buildAuthUrlV2($serverInfo); + } + + return $this->buildAuthUrlV1($serverInfo); + } + + /** + * Performs OpenID verification with the OP. + * @param boolean $validateRequiredAttributes whether to validate required attributes. + * @return boolean whether the verification was successful. + */ + public function validate($validateRequiredAttributes = true) + { + $claimedId = $this->getClaimedId(); + if (empty($claimedId)) { + return false; + } + $params = [ + 'openid.assoc_handle' => $this->data['openid_assoc_handle'], + 'openid.signed' => $this->data['openid_signed'], + 'openid.sig' => $this->data['openid_sig'], + ]; + + if (isset($this->data['openid_ns'])) { + /* We're dealing with an OpenID 2.0 server, so let's set an ns + Even though we should know location of the endpoint, + we still need to verify it by discovery, so $server is not set here*/ + $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; + } elseif (isset($this->data['openid_claimed_id']) && $this->data['openid_claimed_id'] != $this->data['openid_identity']) { + // If it's an OpenID 1 provider, and we've got claimed_id, + // we have to append it to the returnUrl, like authUrlV1 does. + $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $claimedId; + } + + if ($this->data['openid_return_to'] != $this->returnUrl) { + // The return_to url must match the url of current request. + return false; + } + + $serverInfo = $this->discover($claimedId); + + foreach (explode(',', $this->data['openid_signed']) as $item) { + $value = $this->data['openid_' . str_replace('.', '_', $item)]; + $params['openid.' . $item] = $value; + } + + $params['openid.mode'] = 'check_authentication'; + + $response = $this->sendRequest($serverInfo['url'], 'POST', $params); + + if (preg_match('/is_valid\s*:\s*true/i', $response)) { + if ($validateRequiredAttributes) { + return $this->validateRequiredAttributes(); + } else { + return true; + } + } else { + return false; + } + } + + /** + * Checks if all required attributes are present in the server response. + * @return boolean whether all required attributes are present. + */ + protected function validateRequiredAttributes() + { + if (!empty($this->requiredAttributes)) { + $attributes = $this->fetchAttributes(); + foreach ($this->requiredAttributes as $openIdAttributeName) { + if (!isset($attributes[$openIdAttributeName])) { + return false; + } + } + } + + return true; + } + + /** + * Gets AX attributes provided by OP. + * @return array array of attributes. + */ + protected function fetchAxAttributes() + { + $alias = null; + if (isset($this->data['openid_ns_ax']) && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0') { + // It's the most likely case, so we'll check it before + $alias = 'ax'; + } else { + // 'ax' prefix is either undefined, or points to another extension, so we search for another prefix + foreach ($this->data as $key => $value) { + if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_' && $value == 'http://openid.net/srv/ax/1.0') { + $alias = substr($key, strlen('openid_ns_')); + break; + } + } + } + if (!$alias) { + // An alias for AX schema has not been found, so there is no AX data in the OP's response + return []; + } + + $attributes = []; + foreach ($this->data as $key => $value) { + $keyMatch = 'openid_' . $alias . '_value_'; + if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($this->data['openid_' . $alias . '_type_' . $key])) { + /* OP is breaking the spec by returning a field without + associated ns. This shouldn't happen, but it's better + to check, than cause an E_NOTICE.*/ + continue; + } + $key = substr($this->data['openid_' . $alias . '_type_' . $key], strlen('http://axschema.org/')); + $attributes[$key] = $value; + } + + return $attributes; + } + + /** + * Gets SREG attributes provided by OP. SREG names will be mapped to AX names. + * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' + */ + protected function fetchSregAttributes() + { + $attributes = []; + $sregToAx = array_flip($this->axToSregMap); + foreach ($this->data as $key => $value) { + $keyMatch = 'openid_sreg_'; + if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($sregToAx[$key])) { + // The field name isn't part of the SREG spec, so we ignore it. + continue; + } + $attributes[$sregToAx[$key]] = $value; + } + + return $attributes; + } + + /** + * Gets AX/SREG attributes provided by OP. Should be used only after successful validation. + * Note that it does not guarantee that any of the required/optional parameters will be present, + * or that there will be no other attributes besides those specified. + * In other words. OP may provide whatever information it wants to. + * SREG names will be mapped to AX names. + * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' + * @see http://www.axschema.org/types/ + */ + public function fetchAttributes() + { + if (isset($this->data['openid_ns']) && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0') { + // OpenID 2.0 + // We search for both AX and SREG attributes, with AX taking precedence. + return array_merge($this->fetchSregAttributes(), $this->fetchAxAttributes()); + } + + return $this->fetchSregAttributes(); + } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return array_merge(['id' => $this->getClaimedId()], $this->fetchAttributes()); + } } diff --git a/extensions/authclient/clients/Facebook.php b/extensions/authclient/clients/Facebook.php index fd59ae0b4fb..649af8b4ce5 100644 --- a/extensions/authclient/clients/Facebook.php +++ b/extensions/authclient/clients/Facebook.php @@ -40,44 +40,44 @@ */ class Facebook extends OAuth2 { - /** - * @inheritdoc - */ - public $authUrl = 'https://www.facebook.com/dialog/oauth'; - /** - * @inheritdoc - */ - public $tokenUrl = 'https://graph.facebook.com/oauth/access_token'; - /** - * @inheritdoc - */ - public $apiBaseUrl = 'https://graph.facebook.com'; - /** - * @inheritdoc - */ - public $scope = 'email'; + /** + * @inheritdoc + */ + public $authUrl = 'https://www.facebook.com/dialog/oauth'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://graph.facebook.com/oauth/access_token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://graph.facebook.com'; + /** + * @inheritdoc + */ + public $scope = 'email'; - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - return $this->api('me', 'GET'); - } + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('me', 'GET'); + } - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'facebook'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'facebook'; + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'Facebook'; - } + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Facebook'; + } } diff --git a/extensions/authclient/clients/GitHub.php b/extensions/authclient/clients/GitHub.php index a828dfe0a67..0350738d49a 100644 --- a/extensions/authclient/clients/GitHub.php +++ b/extensions/authclient/clients/GitHub.php @@ -40,54 +40,54 @@ */ class GitHub extends OAuth2 { - /** - * @inheritdoc - */ - public $authUrl = 'https://github.com/login/oauth/authorize'; - /** - * @inheritdoc - */ - public $tokenUrl = 'https://github.com/login/oauth/access_token'; - /** - * @inheritdoc - */ - public $apiBaseUrl = 'https://api.github.com'; + /** + * @inheritdoc + */ + public $authUrl = 'https://github.com/login/oauth/authorize'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://github.com/login/oauth/access_token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://api.github.com'; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->scope === null) { - $this->scope = implode(' ', [ - 'user', - 'user:email', - ]); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->scope === null) { + $this->scope = implode(' ', [ + 'user', + 'user:email', + ]); + } + } - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - return $this->api('user', 'GET'); - } + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('user', 'GET'); + } - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'github'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'github'; + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'GitHub'; - } + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'GitHub'; + } } diff --git a/extensions/authclient/clients/GoogleOAuth.php b/extensions/authclient/clients/GoogleOAuth.php index 5044232bbc2..d8bc591c191 100644 --- a/extensions/authclient/clients/GoogleOAuth.php +++ b/extensions/authclient/clients/GoogleOAuth.php @@ -40,54 +40,54 @@ */ class GoogleOAuth extends OAuth2 { - /** - * @inheritdoc - */ - public $authUrl = 'https://accounts.google.com/o/oauth2/auth'; - /** - * @inheritdoc - */ - public $tokenUrl = 'https://accounts.google.com/o/oauth2/token'; - /** - * @inheritdoc - */ - public $apiBaseUrl = 'https://www.googleapis.com/oauth2/v1'; + /** + * @inheritdoc + */ + public $authUrl = 'https://accounts.google.com/o/oauth2/auth'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://accounts.google.com/o/oauth2/token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://www.googleapis.com/oauth2/v1'; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->scope === null) { - $this->scope = implode(' ', [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email', - ]); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->scope === null) { + $this->scope = implode(' ', [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + ]); + } + } - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - return $this->api('userinfo', 'GET'); - } + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('userinfo', 'GET'); + } - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'google'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'google'; + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'Google'; - } + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Google'; + } } diff --git a/extensions/authclient/clients/GoogleOpenId.php b/extensions/authclient/clients/GoogleOpenId.php index 9f7035faa2b..9bcc61c9718 100644 --- a/extensions/authclient/clients/GoogleOpenId.php +++ b/extensions/authclient/clients/GoogleOpenId.php @@ -34,57 +34,57 @@ */ class GoogleOpenId extends OpenId { - /** - * @inheritdoc - */ - public $authUrl = 'https://www.google.com/accounts/o8/id'; - /** - * @inheritdoc - */ - public $requiredAttributes = [ - 'namePerson/first', - 'namePerson/last', - 'contact/email', - 'pref/language', - ]; + /** + * @inheritdoc + */ + public $authUrl = 'https://www.google.com/accounts/o8/id'; + /** + * @inheritdoc + */ + public $requiredAttributes = [ + 'namePerson/first', + 'namePerson/last', + 'contact/email', + 'pref/language', + ]; - /** - * @inheritdoc - */ - protected function defaultNormalizeUserAttributeMap() - { - return [ - 'first_name' => 'namePerson/first', - 'last_name' => 'namePerson/last', - 'email' => 'contact/email', - 'language' => 'pref/language', - ]; - } + /** + * @inheritdoc + */ + protected function defaultNormalizeUserAttributeMap() + { + return [ + 'first_name' => 'namePerson/first', + 'last_name' => 'namePerson/last', + 'email' => 'contact/email', + 'language' => 'pref/language', + ]; + } - /** - * @inheritdoc - */ - protected function defaultViewOptions() - { - return [ - 'popupWidth' => 880, - 'popupHeight' => 520, - ]; - } + /** + * @inheritdoc + */ + protected function defaultViewOptions() + { + return [ + 'popupWidth' => 880, + 'popupHeight' => 520, + ]; + } - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'google'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'google'; + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'Google'; - } + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Google'; + } } diff --git a/extensions/authclient/clients/LinkedIn.php b/extensions/authclient/clients/LinkedIn.php index afd763a86af..78d78d5a838 100644 --- a/extensions/authclient/clients/LinkedIn.php +++ b/extensions/authclient/clients/LinkedIn.php @@ -43,128 +43,133 @@ */ class LinkedIn extends OAuth2 { - /** - * @inheritdoc - */ - public $authUrl = 'https://www.linkedin.com/uas/oauth2/authorization'; - /** - * @inheritdoc - */ - public $tokenUrl = 'https://www.linkedin.com/uas/oauth2/accessToken'; - /** - * @inheritdoc - */ - public $apiBaseUrl = 'https://api.linkedin.com/v1'; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->scope === null) { - $this->scope = implode(' ', [ - 'r_basicprofile', - 'r_emailaddress', - ]); - } - } - - /** - * @inheritdoc - */ - protected function defaultNormalizeUserAttributeMap() - { - return [ - 'email' => 'email-address', - 'first_name' => 'first-name', - 'last_name' => 'last-name', - ]; - } - - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - $attributeNames = [ - 'id', - 'email-address', - 'first-name', - 'last-name', - 'public-profile-url', - ]; - return $this->api('people/~:(' . implode(',', $attributeNames) . ')', 'GET'); - } - - /** - * @inheritdoc - */ - public function buildAuthUrl(array $params = []) - { - $authState = $this->generateAuthState(); - $this->setState('authState', $authState); - $params['state'] = $authState; - return parent::buildAuthUrl($params); - } - - /** - * @inheritdoc - */ - public function fetchAccessToken($authCode, array $params = []) - { - $authState = $this->getState('authState'); - if (!isset($_REQUEST['state']) || empty($authState) || strcmp($_REQUEST['state'], $authState) !== 0) { - throw new HttpException(400, 'Invalid auth state parameter.'); - } else { - $this->removeState('authState'); - } - return parent::fetchAccessToken($authCode, $params); - } - - /** - * @inheritdoc - */ - protected function apiInternal($accessToken, $url, $method, array $params) - { - $params['oauth2_access_token'] = $accessToken->getToken(); - return $this->sendRequest($method, $url, $params); - } - - /** - * @inheritdoc - */ - protected function defaultReturnUrl() - { - $params = $_GET; - unset($params['code']); - unset($params['state']); - $params[0] = Yii::$app->controller->getRoute(); - return Yii::$app->getUrlManager()->createAbsoluteUrl($params); - } - - /** - * Generates the auth state value. - * @return string auth state value. - */ - protected function generateAuthState() - { - return sha1(uniqid(get_class($this), true)); - } - - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'linkedin'; - } - - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'LinkedIn'; - } + /** + * @inheritdoc + */ + public $authUrl = 'https://www.linkedin.com/uas/oauth2/authorization'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://www.linkedin.com/uas/oauth2/accessToken'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://api.linkedin.com/v1'; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->scope === null) { + $this->scope = implode(' ', [ + 'r_basicprofile', + 'r_emailaddress', + ]); + } + } + + /** + * @inheritdoc + */ + protected function defaultNormalizeUserAttributeMap() + { + return [ + 'email' => 'email-address', + 'first_name' => 'first-name', + 'last_name' => 'last-name', + ]; + } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + $attributeNames = [ + 'id', + 'email-address', + 'first-name', + 'last-name', + 'public-profile-url', + ]; + + return $this->api('people/~:(' . implode(',', $attributeNames) . ')', 'GET'); + } + + /** + * @inheritdoc + */ + public function buildAuthUrl(array $params = []) + { + $authState = $this->generateAuthState(); + $this->setState('authState', $authState); + $params['state'] = $authState; + + return parent::buildAuthUrl($params); + } + + /** + * @inheritdoc + */ + public function fetchAccessToken($authCode, array $params = []) + { + $authState = $this->getState('authState'); + if (!isset($_REQUEST['state']) || empty($authState) || strcmp($_REQUEST['state'], $authState) !== 0) { + throw new HttpException(400, 'Invalid auth state parameter.'); + } else { + $this->removeState('authState'); + } + + return parent::fetchAccessToken($authCode, $params); + } + + /** + * @inheritdoc + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + $params['oauth2_access_token'] = $accessToken->getToken(); + + return $this->sendRequest($method, $url, $params); + } + + /** + * @inheritdoc + */ + protected function defaultReturnUrl() + { + $params = $_GET; + unset($params['code']); + unset($params['state']); + $params[0] = Yii::$app->controller->getRoute(); + + return Yii::$app->getUrlManager()->createAbsoluteUrl($params); + } + + /** + * Generates the auth state value. + * @return string auth state value. + */ + protected function generateAuthState() + { + return sha1(uniqid(get_class($this), true)); + } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'linkedin'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'LinkedIn'; + } } diff --git a/extensions/authclient/clients/Twitter.php b/extensions/authclient/clients/Twitter.php index 444de870878..c5e4d34e71b 100644 --- a/extensions/authclient/clients/Twitter.php +++ b/extensions/authclient/clients/Twitter.php @@ -40,52 +40,52 @@ */ class Twitter extends OAuth1 { - /** - * @inheritdoc - */ - public $authUrl = 'https://api.twitter.com/oauth/authorize'; - /** - * @inheritdoc - */ - public $requestTokenUrl = 'https://api.twitter.com/oauth/request_token'; - /** - * @inheritdoc - */ - public $requestTokenMethod = 'POST'; - /** - * @inheritdoc - */ - public $accessTokenUrl = 'https://api.twitter.com/oauth/access_token'; - /** - * @inheritdoc - */ - public $accessTokenMethod = 'POST'; - /** - * @inheritdoc - */ - public $apiBaseUrl = 'https://api.twitter.com/1.1'; + /** + * @inheritdoc + */ + public $authUrl = 'https://api.twitter.com/oauth/authorize'; + /** + * @inheritdoc + */ + public $requestTokenUrl = 'https://api.twitter.com/oauth/request_token'; + /** + * @inheritdoc + */ + public $requestTokenMethod = 'POST'; + /** + * @inheritdoc + */ + public $accessTokenUrl = 'https://api.twitter.com/oauth/access_token'; + /** + * @inheritdoc + */ + public $accessTokenMethod = 'POST'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://api.twitter.com/1.1'; - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - return $this->api('account/verify_credentials.json', 'GET'); - } + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('account/verify_credentials.json', 'GET'); + } - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'twitter'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'twitter'; + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'Twitter'; - } + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Twitter'; + } } diff --git a/extensions/authclient/clients/YandexOAuth.php b/extensions/authclient/clients/YandexOAuth.php index c8148d4c41a..c419fd42591 100644 --- a/extensions/authclient/clients/YandexOAuth.php +++ b/extensions/authclient/clients/YandexOAuth.php @@ -40,52 +40,53 @@ */ class YandexOAuth extends OAuth2 { - /** - * @inheritdoc - */ - public $authUrl = 'https://oauth.yandex.ru/authorize'; - /** - * @inheritdoc - */ - public $tokenUrl = 'https://oauth.yandex.ru/token'; - /** - * @inheritdoc - */ - public $apiBaseUrl = 'https://login.yandex.ru'; + /** + * @inheritdoc + */ + public $authUrl = 'https://oauth.yandex.ru/authorize'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://oauth.yandex.ru/token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://login.yandex.ru'; - /** - * @inheritdoc - */ - protected function initUserAttributes() - { - return $this->api('info', 'GET'); - } + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('info', 'GET'); + } - /** - * @inheritdoc - */ - protected function apiInternal($accessToken, $url, $method, array $params) - { - if (!isset($params['format'])) { - $params['format'] = 'json'; - } - $params['oauth_token'] = $accessToken->getToken(); - return $this->sendRequest($method, $url, $params); - } + /** + * @inheritdoc + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + if (!isset($params['format'])) { + $params['format'] = 'json'; + } + $params['oauth_token'] = $accessToken->getToken(); - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'yandex'; - } + return $this->sendRequest($method, $url, $params); + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'Yandex'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'yandex'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Yandex'; + } } diff --git a/extensions/authclient/clients/YandexOpenId.php b/extensions/authclient/clients/YandexOpenId.php index e4ec9b19be7..0d1c62d351f 100644 --- a/extensions/authclient/clients/YandexOpenId.php +++ b/extensions/authclient/clients/YandexOpenId.php @@ -34,53 +34,53 @@ */ class YandexOpenId extends OpenId { - /** - * @inheritdoc - */ - public $authUrl = 'http://openid.yandex.ru'; - /** - * @inheritdoc - */ - public $requiredAttributes = [ - 'namePerson', - 'contact/email', - ]; + /** + * @inheritdoc + */ + public $authUrl = 'http://openid.yandex.ru'; + /** + * @inheritdoc + */ + public $requiredAttributes = [ + 'namePerson', + 'contact/email', + ]; - /** - * @inheritdoc - */ - protected function defaultNormalizeUserAttributeMap() - { - return [ - 'name' => 'namePerson', - 'email' => 'contact/email', - ]; - } + /** + * @inheritdoc + */ + protected function defaultNormalizeUserAttributeMap() + { + return [ + 'name' => 'namePerson', + 'email' => 'contact/email', + ]; + } - /** - * @inheritdoc - */ - protected function defaultViewOptions() - { - return [ - 'popupWidth' => 900, - 'popupHeight' => 550, - ]; - } + /** + * @inheritdoc + */ + protected function defaultViewOptions() + { + return [ + 'popupWidth' => 900, + 'popupHeight' => 550, + ]; + } - /** - * @inheritdoc - */ - protected function defaultName() - { - return 'yandex'; - } + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'yandex'; + } - /** - * @inheritdoc - */ - protected function defaultTitle() - { - return 'Yandex'; - } + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Yandex'; + } } diff --git a/extensions/authclient/signature/BaseMethod.php b/extensions/authclient/signature/BaseMethod.php index 4b0cd9917f5..c98b019ea8a 100644 --- a/extensions/authclient/signature/BaseMethod.php +++ b/extensions/authclient/signature/BaseMethod.php @@ -17,33 +17,34 @@ */ abstract class BaseMethod extends Object { - /** - * Return the canonical name of the Signature Method. - * @return string method name. - */ - abstract public function getName(); + /** + * Return the canonical name of the Signature Method. + * @return string method name. + */ + abstract public function getName(); - /** - * Generates OAuth request signature. - * @param string $baseString signature base string. - * @param string $key signature key. - * @return string signature string. - */ - abstract public function generateSignature($baseString, $key); + /** + * Generates OAuth request signature. + * @param string $baseString signature base string. + * @param string $key signature key. + * @return string signature string. + */ + abstract public function generateSignature($baseString, $key); - /** - * Verifies given OAuth request. - * @param string $signature signature to be verified. - * @param string $baseString signature base string. - * @param string $key signature key. - * @return boolean success. - */ - public function verify($signature, $baseString, $key) - { - $expectedSignature = $this->generateSignature($baseString, $key); - if (empty($signature) || empty($expectedSignature)) { - return false; - } - return (strcmp($expectedSignature, $signature) === 0); - } + /** + * Verifies given OAuth request. + * @param string $signature signature to be verified. + * @param string $baseString signature base string. + * @param string $key signature key. + * @return boolean success. + */ + public function verify($signature, $baseString, $key) + { + $expectedSignature = $this->generateSignature($baseString, $key); + if (empty($signature) || empty($expectedSignature)) { + return false; + } + + return (strcmp($expectedSignature, $signature) === 0); + } } diff --git a/extensions/authclient/signature/HmacSha1.php b/extensions/authclient/signature/HmacSha1.php index 15885a9dbe8..1e20bf9e39c 100644 --- a/extensions/authclient/signature/HmacSha1.php +++ b/extensions/authclient/signature/HmacSha1.php @@ -19,29 +19,29 @@ */ class HmacSha1 extends BaseMethod { - /** - * @inheritdoc - */ - public function init() - { - if (!function_exists('hash_hmac')) { - throw new NotSupportedException('PHP "Hash" extension is required.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + if (!function_exists('hash_hmac')) { + throw new NotSupportedException('PHP "Hash" extension is required.'); + } + } - /** - * @inheritdoc - */ - public function getName() - { - return 'HMAC-SHA1'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'HMAC-SHA1'; + } - /** - * @inheritdoc - */ - public function generateSignature($baseString, $key) - { - return base64_encode(hash_hmac('sha1', $baseString, $key, true)); - } + /** + * @inheritdoc + */ + public function generateSignature($baseString, $key) + { + return base64_encode(hash_hmac('sha1', $baseString, $key, true)); + } } diff --git a/extensions/authclient/signature/PlainText.php b/extensions/authclient/signature/PlainText.php index 883a7f598a7..d512730d86a 100644 --- a/extensions/authclient/signature/PlainText.php +++ b/extensions/authclient/signature/PlainText.php @@ -15,19 +15,19 @@ */ class PlainText extends BaseMethod { - /** - * @inheritdoc - */ - public function getName() - { - return 'PLAINTEXT'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'PLAINTEXT'; + } - /** - * @inheritdoc - */ - public function generateSignature($baseString, $key) - { - return $key; - } + /** + * @inheritdoc + */ + public function generateSignature($baseString, $key) + { + return $key; + } } diff --git a/extensions/authclient/signature/RsaSha1.php b/extensions/authclient/signature/RsaSha1.php index ef166088eae..044e7abd5f6 100644 --- a/extensions/authclient/signature/RsaSha1.php +++ b/extensions/authclient/signature/RsaSha1.php @@ -23,146 +23,152 @@ */ class RsaSha1 extends BaseMethod { - /** - * @var string OpenSSL private key certificate content. - * This value can be fetched from file specified by {@link privateCertificateFile}. - */ - protected $_privateCertificate; - /** - * @var string OpenSSL public key certificate content. - * This value can be fetched from file specified by {@link publicCertificateFile}. - */ - protected $_publicCertificate; - /** - * @var string path to the file, which holds private key certificate. - */ - public $privateCertificateFile = ''; - /** - * @var string path to the file, which holds public key certificate. - */ - public $publicCertificateFile = ''; - - /** - * @inheritdoc - */ - public function init() - { - if (!function_exists('openssl_sign')) { - throw new NotSupportedException('PHP "OpenSSL" extension is required.'); - } - } - - /** - * @param string $publicCertificate public key certificate content. - */ - public function setPublicCertificate($publicCertificate) - { - $this->_publicCertificate = $publicCertificate; - } - - /** - * @return string public key certificate content. - */ - public function getPublicCertificate() - { - if ($this->_publicCertificate === null) { - $this->_publicCertificate = $this->initPublicCertificate(); - } - return $this->_publicCertificate; - } - - /** - * @param string $privateCertificate private key certificate content. - */ - public function setPrivateCertificate($privateCertificate) - { - $this->_privateCertificate = $privateCertificate; - } - - /** - * @return string private key certificate content. - */ - public function getPrivateCertificate() - { - if ($this->_privateCertificate === null) { - $this->_privateCertificate = $this->initPrivateCertificate(); - } - return $this->_privateCertificate; - } - - /** - * @inheritdoc - */ - public function getName() - { - return 'RSA-SHA1'; - } - - /** - * Creates initial value for {@link publicCertificate}. - * This method will attempt to fetch the certificate value from {@link publicCertificateFile} file. - * @throws InvalidConfigException on failure. - * @return string public certificate content. - */ - protected function initPublicCertificate() - { - if (!empty($this->publicCertificateFile)) { - if (!file_exists($this->publicCertificateFile)) { - throw new InvalidConfigException("Public certificate file '{$this->publicCertificateFile}' does not exist!"); - } - return file_get_contents($this->publicCertificateFile); - } else { - return ''; - } - } - - /** - * Creates initial value for {@link privateCertificate}. - * This method will attempt to fetch the certificate value from {@link privateCertificateFile} file. - * @throws InvalidConfigException on failure. - * @return string private certificate content. - */ - protected function initPrivateCertificate() - { - if (!empty($this->privateCertificateFile)) { - if (!file_exists($this->privateCertificateFile)) { - throw new InvalidConfigException("Private certificate file '{$this->privateCertificateFile}' does not exist!"); - } - return file_get_contents($this->privateCertificateFile); - } else { - return ''; - } - } - - /** - * @inheritdoc - */ - public function generateSignature($baseString, $key) - { - $privateCertificateContent = $this->getPrivateCertificate(); - // Pull the private key ID from the certificate - $privateKeyId = openssl_pkey_get_private($privateCertificateContent); - // Sign using the key - openssl_sign($baseString, $signature, $privateKeyId); - // Release the key resource - openssl_free_key($privateKeyId); - return base64_encode($signature); - } - - /** - * @inheritdoc - */ - public function verify($signature, $baseString, $key) - { - $decodedSignature = base64_decode($signature); - // Fetch the public key cert based on the request - $publicCertificate = $this->getPublicCertificate(); - // Pull the public key ID from the certificate - $publicKeyId = openssl_pkey_get_public($publicCertificate); - // Check the computed signature against the one passed in the query - $verificationResult = openssl_verify($baseString, $decodedSignature, $publicKeyId); - // Release the key resource - openssl_free_key($publicKeyId); - return ($verificationResult == 1); - } + /** + * @var string OpenSSL private key certificate content. + * This value can be fetched from file specified by {@link privateCertificateFile}. + */ + protected $_privateCertificate; + /** + * @var string OpenSSL public key certificate content. + * This value can be fetched from file specified by {@link publicCertificateFile}. + */ + protected $_publicCertificate; + /** + * @var string path to the file, which holds private key certificate. + */ + public $privateCertificateFile = ''; + /** + * @var string path to the file, which holds public key certificate. + */ + public $publicCertificateFile = ''; + + /** + * @inheritdoc + */ + public function init() + { + if (!function_exists('openssl_sign')) { + throw new NotSupportedException('PHP "OpenSSL" extension is required.'); + } + } + + /** + * @param string $publicCertificate public key certificate content. + */ + public function setPublicCertificate($publicCertificate) + { + $this->_publicCertificate = $publicCertificate; + } + + /** + * @return string public key certificate content. + */ + public function getPublicCertificate() + { + if ($this->_publicCertificate === null) { + $this->_publicCertificate = $this->initPublicCertificate(); + } + + return $this->_publicCertificate; + } + + /** + * @param string $privateCertificate private key certificate content. + */ + public function setPrivateCertificate($privateCertificate) + { + $this->_privateCertificate = $privateCertificate; + } + + /** + * @return string private key certificate content. + */ + public function getPrivateCertificate() + { + if ($this->_privateCertificate === null) { + $this->_privateCertificate = $this->initPrivateCertificate(); + } + + return $this->_privateCertificate; + } + + /** + * @inheritdoc + */ + public function getName() + { + return 'RSA-SHA1'; + } + + /** + * Creates initial value for {@link publicCertificate}. + * This method will attempt to fetch the certificate value from {@link publicCertificateFile} file. + * @throws InvalidConfigException on failure. + * @return string public certificate content. + */ + protected function initPublicCertificate() + { + if (!empty($this->publicCertificateFile)) { + if (!file_exists($this->publicCertificateFile)) { + throw new InvalidConfigException("Public certificate file '{$this->publicCertificateFile}' does not exist!"); + } + + return file_get_contents($this->publicCertificateFile); + } else { + return ''; + } + } + + /** + * Creates initial value for {@link privateCertificate}. + * This method will attempt to fetch the certificate value from {@link privateCertificateFile} file. + * @throws InvalidConfigException on failure. + * @return string private certificate content. + */ + protected function initPrivateCertificate() + { + if (!empty($this->privateCertificateFile)) { + if (!file_exists($this->privateCertificateFile)) { + throw new InvalidConfigException("Private certificate file '{$this->privateCertificateFile}' does not exist!"); + } + + return file_get_contents($this->privateCertificateFile); + } else { + return ''; + } + } + + /** + * @inheritdoc + */ + public function generateSignature($baseString, $key) + { + $privateCertificateContent = $this->getPrivateCertificate(); + // Pull the private key ID from the certificate + $privateKeyId = openssl_pkey_get_private($privateCertificateContent); + // Sign using the key + openssl_sign($baseString, $signature, $privateKeyId); + // Release the key resource + openssl_free_key($privateKeyId); + + return base64_encode($signature); + } + + /** + * @inheritdoc + */ + public function verify($signature, $baseString, $key) + { + $decodedSignature = base64_decode($signature); + // Fetch the public key cert based on the request + $publicCertificate = $this->getPublicCertificate(); + // Pull the public key ID from the certificate + $publicKeyId = openssl_pkey_get_public($publicCertificate); + // Check the computed signature against the one passed in the query + $verificationResult = openssl_verify($baseString, $decodedSignature, $publicKeyId); + // Release the key resource + openssl_free_key($publicKeyId); + + return ($verificationResult == 1); + } } diff --git a/extensions/authclient/views/redirect.php b/extensions/authclient/views/redirect.php index 4f713747e49..1f43c143b84 100644 --- a/extensions/authclient/views/redirect.php +++ b/extensions/authclient/views/redirect.php @@ -7,16 +7,17 @@ /* @var $enforceRedirect boolean */ $redirectJavaScript = << - + diff --git a/extensions/authclient/widgets/Choice.php b/extensions/authclient/widgets/Choice.php index 0d5d7625961..d5622a74833 100644 --- a/extensions/authclient/widgets/Choice.php +++ b/extensions/authclient/widgets/Choice.php @@ -60,182 +60,187 @@ */ class Choice extends Widget { - /** - * @var string name of the auth client collection application component. - * This component will be used to fetch services value if it is not set. - */ - public $clientCollection = 'authClientCollection'; - /** - * @var string name of the GET param , which should be used to passed auth client id to URL - * defined by [[baseAuthUrl]]. - */ - public $clientIdGetParamName = 'authclient'; - /** - * @var array the HTML attributes that should be rendered in the div HTML tag representing the container element. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = [ - 'class' => 'auth-clients' - ]; - /** - * @var boolean indicates if popup window should be used instead of direct links. - */ - public $popupMode = true; - /** - * @var boolean indicates if widget content, should be rendered automatically. - * Note: this value automatically set to 'false' at the first call of [[createProviderUrl()]] - */ - public $autoRender = true; - - /** - * @var array configuration for the external clients base authentication URL. - */ - private $_baseAuthUrl; - /** - * @var ClientInterface[] auth providers list. - */ - private $_clients; - - /** - * @param ClientInterface[] $clients auth providers - */ - public function setClients(array $clients) - { - $this->_clients = $clients; - } - - /** - * @return ClientInterface[] auth providers - */ - public function getClients() - { - if ($this->_clients === null) { - $this->_clients = $this->defaultClients(); - } - return $this->_clients; - } - - /** - * @param array $baseAuthUrl base auth URL configuration. - */ - public function setBaseAuthUrl(array $baseAuthUrl) - { - $this->_baseAuthUrl = $baseAuthUrl; - } - - /** - * @return array base auth URL configuration. - */ - public function getBaseAuthUrl() - { - if (!is_array($this->_baseAuthUrl)) { - $this->_baseAuthUrl = $this->defaultBaseAuthUrl(); - } - return $this->_baseAuthUrl; - } - - /** - * Returns default auth clients list. - * @return ClientInterface[] auth clients list. - */ - protected function defaultClients() - { - /** @var $collection \yii\authclient\Collection */ - $collection = Yii::$app->getComponent($this->clientCollection); - return $collection->getClients(); - } - - /** - * Composes default base auth URL configuration. - * @return array base auth URL configuration. - */ - protected function defaultBaseAuthUrl() - { - $baseAuthUrl = [ - Yii::$app->controller->getRoute() - ]; - $params = $_GET; - unset($params[$this->clientIdGetParamName]); - $baseAuthUrl = array_merge($baseAuthUrl, $params); - return $baseAuthUrl; - } - - /** - * Outputs client auth link. - * @param ClientInterface $client external auth client instance. - * @param string $text link text, if not set - default value will be generated. - * @param array $htmlOptions link HTML options. - */ - public function clientLink($client, $text = null, array $htmlOptions = []) - { - if ($text === null) { - $text = Html::tag('span', '', ['class' => 'auth-icon ' . $client->getName()]); - $text .= Html::tag('span', $client->getTitle(), ['class' => 'auth-title']); - } - if (!array_key_exists('class', $htmlOptions)) { - $htmlOptions['class'] = 'auth-link ' . $client->getName(); - } - if ($this->popupMode) { - $viewOptions = $client->getViewOptions(); - if (isset($viewOptions['popupWidth'])) { - $htmlOptions['data-popup-width'] = $viewOptions['popupWidth']; - } - if (isset($viewOptions['popupHeight'])) { - $htmlOptions['data-popup-height'] = $viewOptions['popupHeight']; - } - } - echo Html::a($text, $this->createClientUrl($client), $htmlOptions); - } - - /** - * Composes client auth URL. - * @param ClientInterface $provider external auth client instance. - * @return string auth URL. - */ - public function createClientUrl($provider) - { - $this->autoRender = false; - $url = $this->getBaseAuthUrl(); - $url[$this->clientIdGetParamName] = $provider->getId(); - return Url::to($url); - } - - /** - * Renders the main content, which includes all external services links. - */ - protected function renderMainContent() - { - echo Html::beginTag('ul', ['class' => 'auth-clients clear']); - foreach ($this->getClients() as $externalService) { - echo Html::beginTag('li', ['class' => 'auth-client']); - $this->clientLink($externalService); - echo Html::endTag('li'); - } - echo Html::endTag('ul'); - } - - /** - * Initializes the widget. - */ - public function init() - { - if ($this->popupMode) { - $view = Yii::$app->getView(); - ChoiceAsset::register($view); - $view->registerJs("\$('#" . $this->getId() . "').authchoice();"); - } - $this->options['id'] = $this->getId(); - echo Html::beginTag('div', $this->options); - } - - /** - * Runs the widget. - */ - public function run() - { - if ($this->autoRender) { - $this->renderMainContent(); - } - echo Html::endTag('div'); - } + /** + * @var string name of the auth client collection application component. + * This component will be used to fetch services value if it is not set. + */ + public $clientCollection = 'authClientCollection'; + /** + * @var string name of the GET param , which should be used to passed auth client id to URL + * defined by [[baseAuthUrl]]. + */ + public $clientIdGetParamName = 'authclient'; + /** + * @var array the HTML attributes that should be rendered in the div HTML tag representing the container element. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = [ + 'class' => 'auth-clients' + ]; + /** + * @var boolean indicates if popup window should be used instead of direct links. + */ + public $popupMode = true; + /** + * @var boolean indicates if widget content, should be rendered automatically. + * Note: this value automatically set to 'false' at the first call of [[createProviderUrl()]] + */ + public $autoRender = true; + + /** + * @var array configuration for the external clients base authentication URL. + */ + private $_baseAuthUrl; + /** + * @var ClientInterface[] auth providers list. + */ + private $_clients; + + /** + * @param ClientInterface[] $clients auth providers + */ + public function setClients(array $clients) + { + $this->_clients = $clients; + } + + /** + * @return ClientInterface[] auth providers + */ + public function getClients() + { + if ($this->_clients === null) { + $this->_clients = $this->defaultClients(); + } + + return $this->_clients; + } + + /** + * @param array $baseAuthUrl base auth URL configuration. + */ + public function setBaseAuthUrl(array $baseAuthUrl) + { + $this->_baseAuthUrl = $baseAuthUrl; + } + + /** + * @return array base auth URL configuration. + */ + public function getBaseAuthUrl() + { + if (!is_array($this->_baseAuthUrl)) { + $this->_baseAuthUrl = $this->defaultBaseAuthUrl(); + } + + return $this->_baseAuthUrl; + } + + /** + * Returns default auth clients list. + * @return ClientInterface[] auth clients list. + */ + protected function defaultClients() + { + /** @var $collection \yii\authclient\Collection */ + $collection = Yii::$app->getComponent($this->clientCollection); + + return $collection->getClients(); + } + + /** + * Composes default base auth URL configuration. + * @return array base auth URL configuration. + */ + protected function defaultBaseAuthUrl() + { + $baseAuthUrl = [ + Yii::$app->controller->getRoute() + ]; + $params = $_GET; + unset($params[$this->clientIdGetParamName]); + $baseAuthUrl = array_merge($baseAuthUrl, $params); + + return $baseAuthUrl; + } + + /** + * Outputs client auth link. + * @param ClientInterface $client external auth client instance. + * @param string $text link text, if not set - default value will be generated. + * @param array $htmlOptions link HTML options. + */ + public function clientLink($client, $text = null, array $htmlOptions = []) + { + if ($text === null) { + $text = Html::tag('span', '', ['class' => 'auth-icon ' . $client->getName()]); + $text .= Html::tag('span', $client->getTitle(), ['class' => 'auth-title']); + } + if (!array_key_exists('class', $htmlOptions)) { + $htmlOptions['class'] = 'auth-link ' . $client->getName(); + } + if ($this->popupMode) { + $viewOptions = $client->getViewOptions(); + if (isset($viewOptions['popupWidth'])) { + $htmlOptions['data-popup-width'] = $viewOptions['popupWidth']; + } + if (isset($viewOptions['popupHeight'])) { + $htmlOptions['data-popup-height'] = $viewOptions['popupHeight']; + } + } + echo Html::a($text, $this->createClientUrl($client), $htmlOptions); + } + + /** + * Composes client auth URL. + * @param ClientInterface $provider external auth client instance. + * @return string auth URL. + */ + public function createClientUrl($provider) + { + $this->autoRender = false; + $url = $this->getBaseAuthUrl(); + $url[$this->clientIdGetParamName] = $provider->getId(); + + return Url::to($url); + } + + /** + * Renders the main content, which includes all external services links. + */ + protected function renderMainContent() + { + echo Html::beginTag('ul', ['class' => 'auth-clients clear']); + foreach ($this->getClients() as $externalService) { + echo Html::beginTag('li', ['class' => 'auth-client']); + $this->clientLink($externalService); + echo Html::endTag('li'); + } + echo Html::endTag('ul'); + } + + /** + * Initializes the widget. + */ + public function init() + { + if ($this->popupMode) { + $view = Yii::$app->getView(); + ChoiceAsset::register($view); + $view->registerJs("\$('#" . $this->getId() . "').authchoice();"); + } + $this->options['id'] = $this->getId(); + echo Html::beginTag('div', $this->options); + } + + /** + * Runs the widget. + */ + public function run() + { + if ($this->autoRender) { + $this->renderMainContent(); + } + echo Html::endTag('div'); + } } diff --git a/extensions/authclient/widgets/ChoiceAsset.php b/extensions/authclient/widgets/ChoiceAsset.php index ecea8bd9014..f15ff7b8e02 100644 --- a/extensions/authclient/widgets/ChoiceAsset.php +++ b/extensions/authclient/widgets/ChoiceAsset.php @@ -17,14 +17,14 @@ */ class ChoiceAsset extends AssetBundle { - public $sourcePath = '@yii/authclient/widgets/assets'; - public $js = [ - 'authchoice.js', - ]; - public $css = [ - 'authchoice.css', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@yii/authclient/widgets/assets'; + public $js = [ + 'authchoice.js', + ]; + public $css = [ + 'authchoice.css', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/extensions/bootstrap/Alert.php b/extensions/bootstrap/Alert.php index 15b33428acc..60c34c844d4 100644 --- a/extensions/bootstrap/Alert.php +++ b/extensions/bootstrap/Alert.php @@ -46,105 +46,105 @@ */ class Alert extends Widget { - /** - * @var string the body content in the alert component. Note that anything between - * the [[begin()]] and [[end()]] calls of the Alert widget will also be treated - * as the body content, and will be rendered before this. - */ - public $body; - /** - * @var array the options for rendering the close button tag. - * The close button is displayed in the header of the modal window. Clicking - * on the button will hide the modal window. If this is null, no close button will be rendered. - * - * The following special options are supported: - * - * - tag: string, the tag name of the button. Defaults to 'button'. - * - label: string, the label of the button. Defaults to '×'. - * - * The rest of the options will be rendered as the HTML attributes of the button tag. - * Please refer to the [Alert documentation](http://getbootstrap.com/components/#alerts) - * for the supported HTML attributes. - */ - public $closeButton = []; + /** + * @var string the body content in the alert component. Note that anything between + * the [[begin()]] and [[end()]] calls of the Alert widget will also be treated + * as the body content, and will be rendered before this. + */ + public $body; + /** + * @var array the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is null, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to '×'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Alert documentation](http://getbootstrap.com/components/#alerts) + * for the supported HTML attributes. + */ + public $closeButton = []; + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); + $this->initOptions(); - $this->initOptions(); + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderBodyBegin() . "\n"; + } - echo Html::beginTag('div', $this->options) . "\n"; - echo $this->renderBodyBegin() . "\n"; - } + /** + * Renders the widget. + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . Html::endTag('div'); - /** - * Renders the widget. - */ - public function run() - { - echo "\n" . $this->renderBodyEnd(); - echo "\n" . Html::endTag('div'); + $this->registerPlugin('alert'); + } - $this->registerPlugin('alert'); - } + /** + * Renders the close button if any before rendering the content. + * @return string the rendering result + */ + protected function renderBodyBegin() + { + return $this->renderCloseButton(); + } - /** - * Renders the close button if any before rendering the content. - * @return string the rendering result - */ - protected function renderBodyBegin() - { - return $this->renderCloseButton(); - } + /** + * Renders the alert body (if any). + * @return string the rendering result + */ + protected function renderBodyEnd() + { + return $this->body . "\n"; + } - /** - * Renders the alert body (if any). - * @return string the rendering result - */ - protected function renderBodyEnd() - { - return $this->body . "\n"; - } + /** + * Renders the close button. + * @return string the rendering result + */ + protected function renderCloseButton() + { + if ($this->closeButton !== null) { + $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($this->closeButton, 'label', '×'); + if ($tag === 'button' && !isset($this->closeButton['type'])) { + $this->closeButton['type'] = 'button'; + } - /** - * Renders the close button. - * @return string the rendering result - */ - protected function renderCloseButton() - { - if ($this->closeButton !== null) { - $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button'); - $label = ArrayHelper::remove($this->closeButton, 'label', '×'); - if ($tag === 'button' && !isset($this->closeButton['type'])) { - $this->closeButton['type'] = 'button'; - } - return Html::tag($tag, $label, $this->closeButton); - } else { - return null; - } - } + return Html::tag($tag, $label, $this->closeButton); + } else { + return null; + } + } - /** - * Initializes the widget options. - * This method sets the default values for various options. - */ - protected function initOptions() - { - Html::addCssClass($this->options, 'alert'); - Html::addCssClass($this->options, 'fade'); - Html::addCssClass($this->options, 'in'); + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + Html::addCssClass($this->options, 'alert'); + Html::addCssClass($this->options, 'fade'); + Html::addCssClass($this->options, 'in'); - if ($this->closeButton !== null) { - $this->closeButton = array_merge([ - 'data-dismiss' => 'alert', - 'aria-hidden' => 'true', - 'class' => 'close', - ], $this->closeButton); - } - } + if ($this->closeButton !== null) { + $this->closeButton = array_merge([ + 'data-dismiss' => 'alert', + 'aria-hidden' => 'true', + 'class' => 'close', + ], $this->closeButton); + } + } } diff --git a/extensions/bootstrap/BootstrapAsset.php b/extensions/bootstrap/BootstrapAsset.php index 93f7728e382..d5b12443b71 100644 --- a/extensions/bootstrap/BootstrapAsset.php +++ b/extensions/bootstrap/BootstrapAsset.php @@ -17,8 +17,8 @@ */ class BootstrapAsset extends AssetBundle { - public $sourcePath = '@vendor/twbs/bootstrap/dist'; - public $css = [ - 'css/bootstrap.css', - ]; + public $sourcePath = '@vendor/twbs/bootstrap/dist'; + public $css = [ + 'css/bootstrap.css', + ]; } diff --git a/extensions/bootstrap/BootstrapPluginAsset.php b/extensions/bootstrap/BootstrapPluginAsset.php index c451ff4f434..13aa16217a6 100644 --- a/extensions/bootstrap/BootstrapPluginAsset.php +++ b/extensions/bootstrap/BootstrapPluginAsset.php @@ -17,12 +17,12 @@ */ class BootstrapPluginAsset extends AssetBundle { - public $sourcePath = '@vendor/twbs/bootstrap/dist'; - public $js = [ - 'js/bootstrap.js', - ]; - public $depends = [ - 'yii\web\JqueryAsset', - 'yii\bootstrap\BootstrapAsset', - ]; + public $sourcePath = '@vendor/twbs/bootstrap/dist'; + public $js = [ + 'js/bootstrap.js', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + 'yii\bootstrap\BootstrapAsset', + ]; } diff --git a/extensions/bootstrap/BootstrapThemeAsset.php b/extensions/bootstrap/BootstrapThemeAsset.php index fa424f934a7..093350cc990 100644 --- a/extensions/bootstrap/BootstrapThemeAsset.php +++ b/extensions/bootstrap/BootstrapThemeAsset.php @@ -17,11 +17,11 @@ */ class BootstrapThemeAsset extends AssetBundle { - public $sourcePath = '@vendor/twbs/bootstrap/dist'; - public $css = [ - 'css/bootstrap-theme.css', - ]; - public $depends = [ - 'yii\bootstrap\BootstrapAsset', - ]; + public $sourcePath = '@vendor/twbs/bootstrap/dist'; + public $css = [ + 'css/bootstrap-theme.css', + ]; + public $depends = [ + 'yii\bootstrap\BootstrapAsset', + ]; } diff --git a/extensions/bootstrap/Button.php b/extensions/bootstrap/Button.php index e1af48950dd..d13c9e9bfea 100644 --- a/extensions/bootstrap/Button.php +++ b/extensions/bootstrap/Button.php @@ -26,37 +26,36 @@ */ class Button extends Widget { - /** - * @var string the tag to use to render the button - */ - public $tagName = 'button'; - /** - * @var string the button label - */ - public $label = 'Button'; - /** - * @var boolean whether the label should be HTML-encoded. - */ - public $encodeLabel = true; + /** + * @var string the tag to use to render the button + */ + public $tagName = 'button'; + /** + * @var string the button label + */ + public $label = 'Button'; + /** + * @var boolean whether the label should be HTML-encoded. + */ + public $encodeLabel = true; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + $this->clientOptions = false; + Html::addCssClass($this->options, 'btn'); + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - $this->clientOptions = false; - Html::addCssClass($this->options, 'btn'); - } - - /** - * Renders the widget. - */ - public function run() - { - echo Html::tag($this->tagName, $this->encodeLabel ? Html::encode($this->label) : $this->label, $this->options); - $this->registerPlugin('button'); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::tag($this->tagName, $this->encodeLabel ? Html::encode($this->label) : $this->label, $this->options); + $this->registerPlugin('button'); + } } diff --git a/extensions/bootstrap/ButtonDropdown.php b/extensions/bootstrap/ButtonDropdown.php index 434f214e58c..dedf8db2676 100644 --- a/extensions/bootstrap/ButtonDropdown.php +++ b/extensions/bootstrap/ButtonDropdown.php @@ -33,91 +33,92 @@ */ class ButtonDropdown extends Widget { - /** - * @var string the button label - */ - public $label = 'Button'; - /** - * @var array the HTML attributes of the button. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array the configuration array for [[Dropdown]]. - */ - public $dropdown = []; - /** - * @var boolean whether to display a group of split-styled button group. - */ - public $split = false; - /** - * @var string the tag to use to render the button - */ - public $tagName = 'button'; - /** - * @var boolean whether the label should be HTML-encoded. - */ - public $encodeLabel = true; + /** + * @var string the button label + */ + public $label = 'Button'; + /** + * @var array the HTML attributes of the button. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the configuration array for [[Dropdown]]. + */ + public $dropdown = []; + /** + * @var boolean whether to display a group of split-styled button group. + */ + public $split = false; + /** + * @var string the tag to use to render the button + */ + public $tagName = 'button'; + /** + * @var boolean whether the label should be HTML-encoded. + */ + public $encodeLabel = true; + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderButton() . "\n" . $this->renderDropdown(); + $this->registerPlugin('button'); + } - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderButton() . "\n" . $this->renderDropdown(); - $this->registerPlugin('button'); - } + /** + * Generates the button dropdown. + * @return string the rendering result. + */ + protected function renderButton() + { + Html::addCssClass($this->options, 'btn'); + $label = $this->label; + if ($this->encodeLabel) { + $label = Html::encode($label); + } + if ($this->split) { + $options = $this->options; + $this->options['data-toggle'] = 'dropdown'; + Html::addCssClass($this->options, 'dropdown-toggle'); + $splitButton = Button::widget([ + 'label' => '', + 'encodeLabel' => false, + 'options' => $this->options, + 'view' => $this->getView(), + ]); + } else { + $label .= ' '; + $options = $this->options; + if (!isset($options['href'])) { + $options['href'] = '#'; + } + Html::addCssClass($options, 'dropdown-toggle'); + $options['data-toggle'] = 'dropdown'; + $splitButton = ''; + } - /** - * Generates the button dropdown. - * @return string the rendering result. - */ - protected function renderButton() - { - Html::addCssClass($this->options, 'btn'); - $label = $this->label; - if ($this->encodeLabel) { - $label = Html::encode($label); - } - if ($this->split) { - $options = $this->options; - $this->options['data-toggle'] = 'dropdown'; - Html::addCssClass($this->options, 'dropdown-toggle'); - $splitButton = Button::widget([ - 'label' => '', - 'encodeLabel' => false, - 'options' => $this->options, - 'view' => $this->getView(), - ]); - } else { - $label .= ' '; - $options = $this->options; - if (!isset($options['href'])) { - $options['href'] = '#'; - } - Html::addCssClass($options, 'dropdown-toggle'); - $options['data-toggle'] = 'dropdown'; - $splitButton = ''; - } - return Button::widget([ - 'tagName' => $this->tagName, - 'label' => $label, - 'options' => $options, - 'encodeLabel' => false, - 'view' => $this->getView(), - ]) . "\n" . $splitButton; - } + return Button::widget([ + 'tagName' => $this->tagName, + 'label' => $label, + 'options' => $options, + 'encodeLabel' => false, + 'view' => $this->getView(), + ]) . "\n" . $splitButton; + } - /** - * Generates the dropdown menu. - * @return string the rendering result. - */ - protected function renderDropdown() - { - $config = $this->dropdown; - $config['clientOptions'] = false; - $config['view'] = $this->getView(); - return Dropdown::widget($config); - } + /** + * Generates the dropdown menu. + * @return string the rendering result. + */ + protected function renderDropdown() + { + $config = $this->dropdown; + $config['clientOptions'] = false; + $config['view'] = $this->getView(); + + return Dropdown::widget($config); + } } diff --git a/extensions/bootstrap/ButtonGroup.php b/extensions/bootstrap/ButtonGroup.php index 66d2cc07bda..8b08b6103ac 100644 --- a/extensions/bootstrap/ButtonGroup.php +++ b/extensions/bootstrap/ButtonGroup.php @@ -39,60 +39,60 @@ */ class ButtonGroup extends Widget { - /** - * @var array list of buttons. Each array element represents a single button - * which can be specified as a string or an array of the following structure: - * - * - label: string, required, the button label. - * - options: array, optional, the HTML attributes of the button. - */ - public $buttons = []; - /** - * @var boolean whether to HTML-encode the button labels. - */ - public $encodeLabels = true; + /** + * @var array list of buttons. Each array element represents a single button + * which can be specified as a string or an array of the following structure: + * + * - label: string, required, the button label. + * - options: array, optional, the HTML attributes of the button. + */ + public $buttons = []; + /** + * @var boolean whether to HTML-encode the button labels. + */ + public $encodeLabels = true; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'btn-group'); + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - Html::addCssClass($this->options, 'btn-group'); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::tag('div', $this->renderButtons(), $this->options); + BootstrapAsset::register($this->getView()); + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::tag('div', $this->renderButtons(), $this->options); - BootstrapAsset::register($this->getView()); - } + /** + * Generates the buttons that compound the group as specified on [[items]]. + * @return string the rendering result. + */ + protected function renderButtons() + { + $buttons = []; + foreach ($this->buttons as $button) { + if (is_array($button)) { + $label = ArrayHelper::getValue($button, 'label'); + $options = ArrayHelper::getValue($button, 'options'); + $buttons[] = Button::widget([ + 'label' => $label, + 'options' => $options, + 'encodeLabel' => $this->encodeLabels, + 'view' => $this->getView() + ]); + } else { + $buttons[] = $button; + } + } - /** - * Generates the buttons that compound the group as specified on [[items]]. - * @return string the rendering result. - */ - protected function renderButtons() - { - $buttons = []; - foreach ($this->buttons as $button) { - if (is_array($button)) { - $label = ArrayHelper::getValue($button, 'label'); - $options = ArrayHelper::getValue($button, 'options'); - $buttons[] = Button::widget([ - 'label' => $label, - 'options' => $options, - 'encodeLabel' => $this->encodeLabels, - 'view' => $this->getView() - ]); - } else { - $buttons[] = $button; - } - } - return implode("\n", $buttons); - } + return implode("\n", $buttons); + } } diff --git a/extensions/bootstrap/Carousel.php b/extensions/bootstrap/Carousel.php index eaeb6a5669b..40291a21275 100644 --- a/extensions/bootstrap/Carousel.php +++ b/extensions/bootstrap/Carousel.php @@ -39,132 +39,133 @@ */ class Carousel extends Widget { - /** - * @var array|boolean the labels for the previous and the next control buttons. - * If false, it means the previous and the next control buttons should not be displayed. - */ - public $controls = ['‹', '›']; - /** - * @var array list of slides in the carousel. Each array element represents a single - * slide with the following structure: - * - * ```php - * [ - * // required, slide content (HTML), such as an image tag - * 'content' => '', - * // optional, the caption (HTML) of the slide - * 'caption' => '

This is title

This is the caption text

', - * // optional the HTML attributes of the slide container - * 'options' => [], - * ] - * ``` - */ - public $items = []; + /** + * @var array|boolean the labels for the previous and the next control buttons. + * If false, it means the previous and the next control buttons should not be displayed. + */ + public $controls = ['‹', '›']; + /** + * @var array list of slides in the carousel. Each array element represents a single + * slide with the following structure: + * + * ```php + * [ + * // required, slide content (HTML), such as an image tag + * 'content' => '', + * // optional, the caption (HTML) of the slide + * 'caption' => '

This is title

This is the caption text

', + * // optional the HTML attributes of the slide container + * 'options' => [], + * ] + * ``` + */ + public $items = []; + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'carousel'); + } - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - Html::addCssClass($this->options, 'carousel'); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderIndicators() . "\n"; + echo $this->renderItems() . "\n"; + echo $this->renderControls() . "\n"; + echo Html::endTag('div') . "\n"; + $this->registerPlugin('carousel'); + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::beginTag('div', $this->options) . "\n"; - echo $this->renderIndicators() . "\n"; - echo $this->renderItems() . "\n"; - echo $this->renderControls() . "\n"; - echo Html::endTag('div') . "\n"; - $this->registerPlugin('carousel'); - } + /** + * Renders carousel indicators. + * @return string the rendering result + */ + public function renderIndicators() + { + $indicators = []; + for ($i = 0, $count = count($this->items); $i < $count; $i++) { + $options = ['data-target' => '#' . $this->options['id'], 'data-slide-to' => $i]; + if ($i === 0) { + Html::addCssClass($options, 'active'); + } + $indicators[] = Html::tag('li', '', $options); + } - /** - * Renders carousel indicators. - * @return string the rendering result - */ - public function renderIndicators() - { - $indicators = []; - for ($i = 0, $count = count($this->items); $i < $count; $i++) { - $options = ['data-target' => '#' . $this->options['id'], 'data-slide-to' => $i]; - if ($i === 0) { - Html::addCssClass($options, 'active'); - } - $indicators[] = Html::tag('li', '', $options); - } - return Html::tag('ol', implode("\n", $indicators), ['class' => 'carousel-indicators']); - } + return Html::tag('ol', implode("\n", $indicators), ['class' => 'carousel-indicators']); + } - /** - * Renders carousel items as specified on [[items]]. - * @return string the rendering result - */ - public function renderItems() - { - $items = []; - for ($i = 0, $count = count($this->items); $i < $count; $i++) { - $items[] = $this->renderItem($this->items[$i], $i); - } - return Html::tag('div', implode("\n", $items), ['class' => 'carousel-inner']); - } + /** + * Renders carousel items as specified on [[items]]. + * @return string the rendering result + */ + public function renderItems() + { + $items = []; + for ($i = 0, $count = count($this->items); $i < $count; $i++) { + $items[] = $this->renderItem($this->items[$i], $i); + } - /** - * Renders a single carousel item - * @param string|array $item a single item from [[items]] - * @param integer $index the item index as the first item should be set to `active` - * @return string the rendering result - * @throws InvalidConfigException if the item is invalid - */ - public function renderItem($item, $index) - { - if (is_string($item)) { - $content = $item; - $caption = null; - $options = []; - } elseif (isset($item['content'])) { - $content = $item['content']; - $caption = ArrayHelper::getValue($item, 'caption'); - if ($caption !== null) { - $caption = Html::tag('div', $caption, ['class' => 'carousel-caption']); - } - $options = ArrayHelper::getValue($item, 'options', []); - } else { - throw new InvalidConfigException('The "content" option is required.'); - } + return Html::tag('div', implode("\n", $items), ['class' => 'carousel-inner']); + } - Html::addCssClass($options, 'item'); - if ($index === 0) { - Html::addCssClass($options, 'active'); - } + /** + * Renders a single carousel item + * @param string|array $item a single item from [[items]] + * @param integer $index the item index as the first item should be set to `active` + * @return string the rendering result + * @throws InvalidConfigException if the item is invalid + */ + public function renderItem($item, $index) + { + if (is_string($item)) { + $content = $item; + $caption = null; + $options = []; + } elseif (isset($item['content'])) { + $content = $item['content']; + $caption = ArrayHelper::getValue($item, 'caption'); + if ($caption !== null) { + $caption = Html::tag('div', $caption, ['class' => 'carousel-caption']); + } + $options = ArrayHelper::getValue($item, 'options', []); + } else { + throw new InvalidConfigException('The "content" option is required.'); + } - return Html::tag('div', $content . "\n" . $caption, $options); - } + Html::addCssClass($options, 'item'); + if ($index === 0) { + Html::addCssClass($options, 'active'); + } - /** - * Renders previous and next control buttons. - * @throws InvalidConfigException if [[controls]] is invalid. - */ - public function renderControls() - { - if (isset($this->controls[0], $this->controls[1])) { - return Html::a($this->controls[0], '#' . $this->options['id'], [ - 'class' => 'left carousel-control', - 'data-slide' => 'prev', - ]) . "\n" - . Html::a($this->controls[1], '#' . $this->options['id'], [ - 'class' => 'right carousel-control', - 'data-slide' => 'next', - ]); - } elseif ($this->controls === false) { - return ''; - } else { - throw new InvalidConfigException('The "controls" property must be either false or an array of two elements.'); - } - } + return Html::tag('div', $content . "\n" . $caption, $options); + } + + /** + * Renders previous and next control buttons. + * @throws InvalidConfigException if [[controls]] is invalid. + */ + public function renderControls() + { + if (isset($this->controls[0], $this->controls[1])) { + return Html::a($this->controls[0], '#' . $this->options['id'], [ + 'class' => 'left carousel-control', + 'data-slide' => 'prev', + ]) . "\n" + . Html::a($this->controls[1], '#' . $this->options['id'], [ + 'class' => 'right carousel-control', + 'data-slide' => 'next', + ]); + } elseif ($this->controls === false) { + return ''; + } else { + throw new InvalidConfigException('The "controls" property must be either false or an array of two elements.'); + } + } } diff --git a/extensions/bootstrap/Collapse.php b/extensions/bootstrap/Collapse.php index 2de06d97a3a..b8fb97c52cc 100644 --- a/extensions/bootstrap/Collapse.php +++ b/extensions/bootstrap/Collapse.php @@ -41,95 +41,94 @@ */ class Collapse extends Widget { - /** - * @var array list of groups in the collapse widget. Each array element represents a single - * group with the following structure: - * - * ```php - * // item key is the actual group header - * 'Collapsible Group Item #1' => [ - * // required, the content (HTML) of the group - * 'content' => 'Anim pariatur cliche...', - * // optional the HTML attributes of the content group - * 'contentOptions' => [], - * // optional the HTML attributes of the group - * 'options' => [], - * ] - * ``` - */ - public $items = []; + /** + * @var array list of groups in the collapse widget. Each array element represents a single + * group with the following structure: + * + * ```php + * // item key is the actual group header + * 'Collapsible Group Item #1' => [ + * // required, the content (HTML) of the group + * 'content' => 'Anim pariatur cliche...', + * // optional the HTML attributes of the content group + * 'contentOptions' => [], + * // optional the HTML attributes of the group + * 'options' => [], + * ] + * ``` + */ + public $items = []; + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'panel-group'); + } - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - Html::addCssClass($this->options, 'panel-group'); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag('div') . "\n"; + $this->registerPlugin('collapse'); + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::beginTag('div', $this->options) . "\n"; - echo $this->renderItems() . "\n"; - echo Html::endTag('div') . "\n"; - $this->registerPlugin('collapse'); - } + /** + * Renders collapsible items as specified on [[items]]. + * @return string the rendering result + */ + public function renderItems() + { + $items = []; + $index = 0; + foreach ($this->items as $header => $item) { + $options = ArrayHelper::getValue($item, 'options', []); + Html::addCssClass($options, 'panel panel-default'); + $items[] = Html::tag('div', $this->renderItem($header, $item, ++$index), $options); + } - /** - * Renders collapsible items as specified on [[items]]. - * @return string the rendering result - */ - public function renderItems() - { - $items = []; - $index = 0; - foreach ($this->items as $header => $item) { - $options = ArrayHelper::getValue($item, 'options', []); - Html::addCssClass($options, 'panel panel-default'); - $items[] = Html::tag('div', $this->renderItem($header, $item, ++$index), $options); - } + return implode("\n", $items); + } - return implode("\n", $items); - } + /** + * Renders a single collapsible item group + * @param string $header a label of the item group [[items]] + * @param array $item a single item from [[items]] + * @param integer $index the item index as each item group content must have an id + * @return string the rendering result + * @throws InvalidConfigException + */ + public function renderItem($header, $item, $index) + { + if (isset($item['content'])) { + $id = $this->options['id'] . '-collapse' . $index; + $options = ArrayHelper::getValue($item, 'contentOptions', []); + $options['id'] = $id; + Html::addCssClass($options, 'panel-collapse collapse'); - /** - * Renders a single collapsible item group - * @param string $header a label of the item group [[items]] - * @param array $item a single item from [[items]] - * @param integer $index the item index as each item group content must have an id - * @return string the rendering result - * @throws InvalidConfigException - */ - public function renderItem($header, $item, $index) - { - if (isset($item['content'])) { - $id = $this->options['id'] . '-collapse' . $index; - $options = ArrayHelper::getValue($item, 'contentOptions', []); - $options['id'] = $id; - Html::addCssClass($options, 'panel-collapse collapse'); + $headerToggle = Html::a($header, '#' . $id, [ + 'class' => 'collapse-toggle', + 'data-toggle' => 'collapse', + 'data-parent' => '#' . $this->options['id'] + ]) . "\n"; - $headerToggle = Html::a($header, '#' . $id, [ - 'class' => 'collapse-toggle', - 'data-toggle' => 'collapse', - 'data-parent' => '#' . $this->options['id'] - ]) . "\n"; + $header = Html::tag('h4', $headerToggle, ['class' => 'panel-title']); - $header = Html::tag('h4', $headerToggle, ['class' => 'panel-title']); + $content = Html::tag('div', $item['content'], ['class' => 'panel-body']) . "\n"; + } else { + throw new InvalidConfigException('The "content" option is required.'); + } + $group = []; - $content = Html::tag('div', $item['content'], ['class' => 'panel-body']) . "\n"; - } else { - throw new InvalidConfigException('The "content" option is required.'); - } - $group = []; + $group[] = Html::tag('div', $header, ['class' => 'panel-heading']); + $group[] = Html::tag('div', $content, $options); - $group[] = Html::tag('div', $header, ['class' => 'panel-heading']); - $group[] = Html::tag('div', $content, $options); - - return implode("\n", $group); - } + return implode("\n", $group); + } } diff --git a/extensions/bootstrap/Dropdown.php b/extensions/bootstrap/Dropdown.php index 85136b862cd..8109add6959 100644 --- a/extensions/bootstrap/Dropdown.php +++ b/extensions/bootstrap/Dropdown.php @@ -20,79 +20,79 @@ */ class Dropdown extends Widget { - /** - * @var array list of menu items in the dropdown. Each array element can be either an HTML string, - * or an array representing a single menu with the following structure: - * - * - label: string, required, the label of the item link - * - url: string, optional, the url of the item link. Defaults to "#". - * - visible: boolean, optional, whether this menu item is visible. Defaults to true. - * - linkOptions: array, optional, the HTML attributes of the item link. - * - options: array, optional, the HTML attributes of the item. - * - items: array, optional, the submenu items. The structure is the same as this property. - * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it. - * - * To insert divider use ``. - */ - public $items = []; - /** - * @var boolean whether the labels for header items should be HTML-encoded. - */ - public $encodeLabels = true; + /** + * @var array list of menu items in the dropdown. Each array element can be either an HTML string, + * or an array representing a single menu with the following structure: + * + * - label: string, required, the label of the item link + * - url: string, optional, the url of the item link. Defaults to "#". + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - linkOptions: array, optional, the HTML attributes of the item link. + * - options: array, optional, the HTML attributes of the item. + * - items: array, optional, the submenu items. The structure is the same as this property. + * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it. + * + * To insert divider use ``. + */ + public $items = []; + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - Html::addCssClass($this->options, 'dropdown-menu'); - } + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'dropdown-menu'); + } - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderItems($this->items); - $this->registerPlugin('dropdown'); - } + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems($this->items); + $this->registerPlugin('dropdown'); + } - /** - * Renders menu items. - * @param array $items the menu items to be rendered - * @return string the rendering result. - * @throws InvalidConfigException if the label option is not specified in one of the items. - */ - protected function renderItems($items) - { - $lines = []; - foreach ($items as $i => $item) { - if (isset($item['visible']) && !$item['visible']) { - unset($items[$i]); - continue; - } - if (is_string($item)) { - $lines[] = $item; - continue; - } - if (!isset($item['label'])) { - throw new InvalidConfigException("The 'label' option is required."); - } - $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; - $options = ArrayHelper::getValue($item, 'options', []); - $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); - $linkOptions['tabindex'] = '-1'; - $content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions); - if (!empty($item['items'])) { - $content .= $this->renderItems($item['items']); - Html::addCssClass($options, 'dropdown-submenu'); - } - $lines[] = Html::tag('li', $content, $options); - } + /** + * Renders menu items. + * @param array $items the menu items to be rendered + * @return string the rendering result. + * @throws InvalidConfigException if the label option is not specified in one of the items. + */ + protected function renderItems($items) + { + $lines = []; + foreach ($items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + if (is_string($item)) { + $lines[] = $item; + continue; + } + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $options = ArrayHelper::getValue($item, 'options', []); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); + $linkOptions['tabindex'] = '-1'; + $content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions); + if (!empty($item['items'])) { + $content .= $this->renderItems($item['items']); + Html::addCssClass($options, 'dropdown-submenu'); + } + $lines[] = Html::tag('li', $content, $options); + } - return Html::tag('ul', implode("\n", $lines), $this->options); - } + return Html::tag('ul', implode("\n", $lines), $this->options); + } } diff --git a/extensions/bootstrap/Modal.php b/extensions/bootstrap/Modal.php index e498f492336..62352788bbf 100644 --- a/extensions/bootstrap/Modal.php +++ b/extensions/bootstrap/Modal.php @@ -35,201 +35,202 @@ */ class Modal extends Widget { - const SIZE_LARGE = "modal-lg"; - const SIZE_SMALL = "modal-sm"; - const SIZE_DEFAULT = ""; - - /** - * @var string the header content in the modal window. - */ - public $header; - /** - * @var string the footer content in the modal window. - */ - public $footer; - /** - * @var string the modal size. Can be MODAL_LG or MODAL_SM, or empty for default. - */ - public $size; - /** - * @var array the options for rendering the close button tag. - * The close button is displayed in the header of the modal window. Clicking - * on the button will hide the modal window. If this is null, no close button will be rendered. - * - * The following special options are supported: - * - * - tag: string, the tag name of the button. Defaults to 'button'. - * - label: string, the label of the button. Defaults to '×'. - * - * The rest of the options will be rendered as the HTML attributes of the button tag. - * Please refer to the [Modal plugin help](http://getbootstrap.com/javascript/#modals) - * for the supported HTML attributes. - */ - public $closeButton = []; - /** - * @var array the options for rendering the toggle button tag. - * The toggle button is used to toggle the visibility of the modal window. - * If this property is null, no toggle button will be rendered. - * - * The following special options are supported: - * - * - tag: string, the tag name of the button. Defaults to 'button'. - * - label: string, the label of the button. Defaults to 'Show'. - * - * The rest of the options will be rendered as the HTML attributes of the button tag. - * Please refer to the [Modal plugin help](http://getbootstrap.com/javascript/#modals) - * for the supported HTML attributes. - */ - public $toggleButton; - - - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - - $this->initOptions(); - - echo $this->renderToggleButton() . "\n"; - echo Html::beginTag('div', $this->options) . "\n"; - echo Html::beginTag('div', ['class' => 'modal-dialog ' . $this->size]) . "\n"; - echo Html::beginTag('div', ['class' => 'modal-content']) . "\n"; - echo $this->renderHeader() . "\n"; - echo $this->renderBodyBegin() . "\n"; - } - - /** - * Renders the widget. - */ - public function run() - { - echo "\n" . $this->renderBodyEnd(); - echo "\n" . $this->renderFooter(); - echo "\n" . Html::endTag('div'); // modal-content - echo "\n" . Html::endTag('div'); // modal-dialog - echo "\n" . Html::endTag('div'); - - $this->registerPlugin('modal'); - } - - /** - * Renders the header HTML markup of the modal - * @return string the rendering result - */ - protected function renderHeader() - { - $button = $this->renderCloseButton(); - if ($button !== null) { - $this->header = $button . "\n" . $this->header; - } - if ($this->header !== null) { - return Html::tag('div', "\n" . $this->header . "\n", ['class' => 'modal-header']); - } else { - return null; - } - } - - /** - * Renders the opening tag of the modal body. - * @return string the rendering result - */ - protected function renderBodyBegin() - { - return Html::beginTag('div', ['class' => 'modal-body']); - } - - /** - * Renders the closing tag of the modal body. - * @return string the rendering result - */ - protected function renderBodyEnd() - { - return Html::endTag('div'); - } - - /** - * Renders the HTML markup for the footer of the modal - * @return string the rendering result - */ - protected function renderFooter() - { - if ($this->footer !== null) { - return Html::tag('div', "\n" . $this->footer . "\n", ['class' => 'modal-footer']); - } else { - return null; - } - } - - /** - * Renders the toggle button. - * @return string the rendering result - */ - protected function renderToggleButton() - { - if ($this->toggleButton !== null) { - $tag = ArrayHelper::remove($this->toggleButton, 'tag', 'button'); - $label = ArrayHelper::remove($this->toggleButton, 'label', 'Show'); - if ($tag === 'button' && !isset($this->toggleButton['type'])) { - $this->toggleButton['type'] = 'button'; - } - return Html::tag($tag, $label, $this->toggleButton); - } else { - return null; - } - } - - /** - * Renders the close button. - * @return string the rendering result - */ - protected function renderCloseButton() - { - if ($this->closeButton !== null) { - $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button'); - $label = ArrayHelper::remove($this->closeButton, 'label', '×'); - if ($tag === 'button' && !isset($this->closeButton['type'])) { - $this->closeButton['type'] = 'button'; - } - return Html::tag($tag, $label, $this->closeButton); - } else { - return null; - } - } - - /** - * Initializes the widget options. - * This method sets the default values for various options. - */ - protected function initOptions() - { - $this->options = array_merge([ - 'class' => 'fade', - 'role' => 'dialog', - 'tabindex' => -1, - ], $this->options); - Html::addCssClass($this->options, 'modal'); - - if ($this->clientOptions !== false) { - $this->clientOptions = array_merge(['show' => false], $this->clientOptions); - } - - if ($this->closeButton !== null) { - $this->closeButton = array_merge([ - 'data-dismiss' => 'modal', - 'aria-hidden' => 'true', - 'class' => 'close', - ], $this->closeButton); - } - - if ($this->toggleButton !== null) { - $this->toggleButton = array_merge([ - 'data-toggle' => 'modal', - ], $this->toggleButton); - if (!isset($this->toggleButton['data-target']) && !isset($this->toggleButton['href'])) { - $this->toggleButton['data-target'] = '#' . $this->options['id']; - } - } - } + const SIZE_LARGE = "modal-lg"; + const SIZE_SMALL = "modal-sm"; + const SIZE_DEFAULT = ""; + + /** + * @var string the header content in the modal window. + */ + public $header; + /** + * @var string the footer content in the modal window. + */ + public $footer; + /** + * @var string the modal size. Can be MODAL_LG or MODAL_SM, or empty for default. + */ + public $size; + /** + * @var array the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is null, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to '×'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](http://getbootstrap.com/javascript/#modals) + * for the supported HTML attributes. + */ + public $closeButton = []; + /** + * @var array the options for rendering the toggle button tag. + * The toggle button is used to toggle the visibility of the modal window. + * If this property is null, no toggle button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to 'Show'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](http://getbootstrap.com/javascript/#modals) + * for the supported HTML attributes. + */ + public $toggleButton; + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo $this->renderToggleButton() . "\n"; + echo Html::beginTag('div', $this->options) . "\n"; + echo Html::beginTag('div', ['class' => 'modal-dialog ' . $this->size]) . "\n"; + echo Html::beginTag('div', ['class' => 'modal-content']) . "\n"; + echo $this->renderHeader() . "\n"; + echo $this->renderBodyBegin() . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . $this->renderFooter(); + echo "\n" . Html::endTag('div'); // modal-content + echo "\n" . Html::endTag('div'); // modal-dialog + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('modal'); + } + + /** + * Renders the header HTML markup of the modal + * @return string the rendering result + */ + protected function renderHeader() + { + $button = $this->renderCloseButton(); + if ($button !== null) { + $this->header = $button . "\n" . $this->header; + } + if ($this->header !== null) { + return Html::tag('div', "\n" . $this->header . "\n", ['class' => 'modal-header']); + } else { + return null; + } + } + + /** + * Renders the opening tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyBegin() + { + return Html::beginTag('div', ['class' => 'modal-body']); + } + + /** + * Renders the closing tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyEnd() + { + return Html::endTag('div'); + } + + /** + * Renders the HTML markup for the footer of the modal + * @return string the rendering result + */ + protected function renderFooter() + { + if ($this->footer !== null) { + return Html::tag('div', "\n" . $this->footer . "\n", ['class' => 'modal-footer']); + } else { + return null; + } + } + + /** + * Renders the toggle button. + * @return string the rendering result + */ + protected function renderToggleButton() + { + if ($this->toggleButton !== null) { + $tag = ArrayHelper::remove($this->toggleButton, 'tag', 'button'); + $label = ArrayHelper::remove($this->toggleButton, 'label', 'Show'); + if ($tag === 'button' && !isset($this->toggleButton['type'])) { + $this->toggleButton['type'] = 'button'; + } + + return Html::tag($tag, $label, $this->toggleButton); + } else { + return null; + } + } + + /** + * Renders the close button. + * @return string the rendering result + */ + protected function renderCloseButton() + { + if ($this->closeButton !== null) { + $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($this->closeButton, 'label', '×'); + if ($tag === 'button' && !isset($this->closeButton['type'])) { + $this->closeButton['type'] = 'button'; + } + + return Html::tag($tag, $label, $this->closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + $this->options = array_merge([ + 'class' => 'fade', + 'role' => 'dialog', + 'tabindex' => -1, + ], $this->options); + Html::addCssClass($this->options, 'modal'); + + if ($this->clientOptions !== false) { + $this->clientOptions = array_merge(['show' => false], $this->clientOptions); + } + + if ($this->closeButton !== null) { + $this->closeButton = array_merge([ + 'data-dismiss' => 'modal', + 'aria-hidden' => 'true', + 'class' => 'close', + ], $this->closeButton); + } + + if ($this->toggleButton !== null) { + $this->toggleButton = array_merge([ + 'data-toggle' => 'modal', + ], $this->toggleButton); + if (!isset($this->toggleButton['data-target']) && !isset($this->toggleButton['href'])) { + $this->toggleButton['data-target'] = '#' . $this->options['id']; + } + } + } } diff --git a/extensions/bootstrap/Nav.php b/extensions/bootstrap/Nav.php index 9d4afc03bd4..191578cd2a9 100644 --- a/extensions/bootstrap/Nav.php +++ b/extensions/bootstrap/Nav.php @@ -48,168 +48,169 @@ */ class Nav extends Widget { - /** - * @var array list of items in the nav widget. Each array element represents a single - * menu item which can be either a string or an array with the following structure: - * - * - label: string, required, the nav item label. - * - url: optional, the item's URL. Defaults to "#". - * - visible: boolean, optional, whether this menu item is visible. Defaults to true. - * - linkOptions: array, optional, the HTML attributes of the item's link. - * - options: array, optional, the HTML attributes of the item container (LI). - * - active: boolean, optional, whether the item should be on active state or not. - * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, - * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. - * - * If a menu item is a string, it will be rendered directly without HTML encoding. - */ - public $items = []; - /** - * @var boolean whether the nav items labels should be HTML-encoded. - */ - public $encodeLabels = true; - /** - * @var boolean whether to automatically activate items according to whether their route setting - * matches the currently requested route. - * @see isItemActive - */ - public $activateItems = true; - /** - * @var string the route used to determine if a menu item is active or not. - * If not set, it will use the route of the current request. - * @see params - * @see isItemActive - */ - public $route; - /** - * @var array the parameters used to determine if a menu item is active or not. - * If not set, it will use `$_GET`. - * @see route - * @see isItemActive - */ - public $params; - - - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - if ($this->route === null && Yii::$app->controller !== null) { - $this->route = Yii::$app->controller->getRoute(); - } - if ($this->params === null) { - $this->params = Yii::$app->request->getQueryParams(); - } - Html::addCssClass($this->options, 'nav'); - } - - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderItems(); - BootstrapAsset::register($this->getView()); - } - - /** - * Renders widget items. - */ - public function renderItems() - { - $items = []; - foreach ($this->items as $i => $item) { - if (isset($item['visible']) && !$item['visible']) { - unset($items[$i]); - continue; - } - $items[] = $this->renderItem($item); - } - - return Html::tag('ul', implode("\n", $items), $this->options); - } - - /** - * Renders a widget's item. - * @param string|array $item the item to render. - * @return string the rendering result. - * @throws InvalidConfigException - */ - public function renderItem($item) - { - if (is_string($item)) { - return $item; - } - if (!isset($item['label'])) { - throw new InvalidConfigException("The 'label' option is required."); - } - $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; - $options = ArrayHelper::getValue($item, 'options', []); - $items = ArrayHelper::getValue($item, 'items'); - $url = ArrayHelper::getValue($item, 'url', '#'); - $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); - - if (isset($item['active'])) { - $active = ArrayHelper::remove($item, 'active', false); - } else { - $active = $this->isItemActive($item); - } - - if ($active) { - Html::addCssClass($options, 'active'); - } - - if ($items !== null) { - $linkOptions['data-toggle'] = 'dropdown'; - Html::addCssClass($options, 'dropdown'); - Html::addCssClass($linkOptions, 'dropdown-toggle'); - $label .= ' ' . Html::tag('b', '', ['class' => 'caret']); - if (is_array($items)) { - $items = Dropdown::widget([ - 'items' => $items, - 'encodeLabels' => $this->encodeLabels, - 'clientOptions' => false, - 'view' => $this->getView(), - ]); - } - } - - return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options); - } - - - /** - * Checks whether a menu item is active. - * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. - * When the `url` option of a menu item is specified in terms of an array, its first element is treated - * as the route for the item and the rest of the elements are the associated parameters. - * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item - * be considered active. - * @param array $item the menu item to be checked - * @return boolean whether the menu item is active - */ - protected function isItemActive($item) - { - if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { - $route = $item['url'][0]; - if ($route[0] !== '/' && Yii::$app->controller) { - $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; - } - if (ltrim($route, '/') !== $this->route) { - return false; - } - unset($item['url']['#']); - if (count($item['url']) > 1) { - foreach (array_splice($item['url'], 1) as $name => $value) { - if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) { - return false; - } - } - } - return true; - } - return false; - } + /** + * @var array list of items in the nav widget. Each array element represents a single + * menu item which can be either a string or an array with the following structure: + * + * - label: string, required, the nav item label. + * - url: optional, the item's URL. Defaults to "#". + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - linkOptions: array, optional, the HTML attributes of the item's link. + * - options: array, optional, the HTML attributes of the item container (LI). + * - active: boolean, optional, whether the item should be on active state or not. + * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, + * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. + * + * If a menu item is a string, it will be rendered directly without HTML encoding. + */ + public $items = []; + /** + * @var boolean whether the nav items labels should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var boolean whether to automatically activate items according to whether their route setting + * matches the currently requested route. + * @see isItemActive + */ + public $activateItems = true; + /** + * @var string the route used to determine if a menu item is active or not. + * If not set, it will use the route of the current request. + * @see params + * @see isItemActive + */ + public $route; + /** + * @var array the parameters used to determine if a menu item is active or not. + * If not set, it will use `$_GET`. + * @see route + * @see isItemActive + */ + public $params; + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + if ($this->route === null && Yii::$app->controller !== null) { + $this->route = Yii::$app->controller->getRoute(); + } + if ($this->params === null) { + $this->params = Yii::$app->request->getQueryParams(); + } + Html::addCssClass($this->options, 'nav'); + } + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems(); + BootstrapAsset::register($this->getView()); + } + + /** + * Renders widget items. + */ + public function renderItems() + { + $items = []; + foreach ($this->items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + $items[] = $this->renderItem($item); + } + + return Html::tag('ul', implode("\n", $items), $this->options); + } + + /** + * Renders a widget's item. + * @param string|array $item the item to render. + * @return string the rendering result. + * @throws InvalidConfigException + */ + public function renderItem($item) + { + if (is_string($item)) { + return $item; + } + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $options = ArrayHelper::getValue($item, 'options', []); + $items = ArrayHelper::getValue($item, 'items'); + $url = ArrayHelper::getValue($item, 'url', '#'); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); + + if (isset($item['active'])) { + $active = ArrayHelper::remove($item, 'active', false); + } else { + $active = $this->isItemActive($item); + } + + if ($active) { + Html::addCssClass($options, 'active'); + } + + if ($items !== null) { + $linkOptions['data-toggle'] = 'dropdown'; + Html::addCssClass($options, 'dropdown'); + Html::addCssClass($linkOptions, 'dropdown-toggle'); + $label .= ' ' . Html::tag('b', '', ['class' => 'caret']); + if (is_array($items)) { + $items = Dropdown::widget([ + 'items' => $items, + 'encodeLabels' => $this->encodeLabels, + 'clientOptions' => false, + 'view' => $this->getView(), + ]); + } + } + + return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options); + } + + + /** + * Checks whether a menu item is active. + * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. + * When the `url` option of a menu item is specified in terms of an array, its first element is treated + * as the route for the item and the rest of the elements are the associated parameters. + * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item + * be considered active. + * @param array $item the menu item to be checked + * @return boolean whether the menu item is active + */ + protected function isItemActive($item) + { + if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { + $route = $item['url'][0]; + if ($route[0] !== '/' && Yii::$app->controller) { + $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; + } + if (ltrim($route, '/') !== $this->route) { + return false; + } + unset($item['url']['#']); + if (count($item['url']) > 1) { + foreach (array_splice($item['url'], 1) as $name => $value) { + if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) { + return false; + } + } + } + + return true; + } + + return false; + } } diff --git a/extensions/bootstrap/NavBar.php b/extensions/bootstrap/NavBar.php index d1396a21e3c..ab681d189f0 100644 --- a/extensions/bootstrap/NavBar.php +++ b/extensions/bootstrap/NavBar.php @@ -39,120 +39,121 @@ */ class NavBar extends Widget { - /** - * @var array the HTML attributes for the widget container tag. The following special options are recognized: - * - * - tag: string, defaults to "nav", the name of the container tag. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array the HTML attributes for the container tag. The following special options are recognized: - * - * - tag: string, defaults to "div", the name of the container tag. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $containerOptions = []; - /** - * @var string the text of the brand. Note that this is not HTML-encoded. - * @see http://getbootstrap.com/components/#navbar - */ - public $brandLabel; - /** - * @param array|string $url the URL for the brand's hyperlink tag. This parameter will be processed by [[Url::to()]] - * and will be used for the "href" attribute of the brand link. If not set, [[\yii\web\Application::homeUrl]] will be used. - */ - public $brandUrl; - /** - * @var array the HTML attributes of the brand link. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $brandOptions = []; - /** - * @var string text to show for screen readers for the button to toggle the navbar. - */ - public $screenReaderToggleText = 'Toggle navigation'; - /** - * @var boolean whether the navbar content should be included in an inner div container which by default - * adds left and right padding. Set this to false for a 100% width navbar. - */ - public $renderInnerContainer = true; - /** - * @var array the HTML attributes of the inner container. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $innerContainerOptions = []; + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "nav", the name of the container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the HTML attributes for the container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the name of the container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $containerOptions = []; + /** + * @var string the text of the brand. Note that this is not HTML-encoded. + * @see http://getbootstrap.com/components/#navbar + */ + public $brandLabel; + /** + * @param array|string $url the URL for the brand's hyperlink tag. This parameter will be processed by [[Url::to()]] + * and will be used for the "href" attribute of the brand link. If not set, [[\yii\web\Application::homeUrl]] will be used. + */ + public $brandUrl; + /** + * @var array the HTML attributes of the brand link. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $brandOptions = []; + /** + * @var string text to show for screen readers for the button to toggle the navbar. + */ + public $screenReaderToggleText = 'Toggle navigation'; + /** + * @var boolean whether the navbar content should be included in an inner div container which by default + * adds left and right padding. Set this to false for a 100% width navbar. + */ + public $renderInnerContainer = true; + /** + * @var array the HTML attributes of the inner container. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $innerContainerOptions = []; - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - $this->clientOptions = false; - Html::addCssClass($this->options, 'navbar'); - if ($this->options['class'] === 'navbar') { - Html::addCssClass($this->options, 'navbar-default'); - } - Html::addCssClass($this->brandOptions, 'navbar-brand'); - if (empty($this->options['role'])) { - $this->options['role'] = 'navigation'; - } - $options = $this->options; - $tag = ArrayHelper::remove($options, 'tag', 'nav'); - echo Html::beginTag($tag, $options); - if ($this->renderInnerContainer) { - if (!isset($this->innerContainerOptions['class'])) { - Html::addCssClass($this->innerContainerOptions, 'container'); - } - echo Html::beginTag('div', $this->innerContainerOptions); - } - echo Html::beginTag('div', ['class' => 'navbar-header']); - if (!isset($this->containerOptions['id'])) { - $this->containerOptions['id'] = "{$this->options['id']}-collapse"; - } - echo $this->renderToggleButton(); - if ($this->brandLabel !== null) { - Html::addCssClass($this->brandOptions, 'navbar-brand'); - echo Html::a($this->brandLabel, $this->brandUrl === null ? Yii::$app->homeUrl : $this->brandUrl, $this->brandOptions); - } - echo Html::endTag('div'); - Html::addCssClass($this->containerOptions, 'collapse'); - Html::addCssClass($this->containerOptions, 'navbar-collapse'); - $options = $this->containerOptions; - $tag = ArrayHelper::remove($options, 'tag', 'div'); - echo Html::beginTag($tag, $options); - } + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + $this->clientOptions = false; + Html::addCssClass($this->options, 'navbar'); + if ($this->options['class'] === 'navbar') { + Html::addCssClass($this->options, 'navbar-default'); + } + Html::addCssClass($this->brandOptions, 'navbar-brand'); + if (empty($this->options['role'])) { + $this->options['role'] = 'navigation'; + } + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'nav'); + echo Html::beginTag($tag, $options); + if ($this->renderInnerContainer) { + if (!isset($this->innerContainerOptions['class'])) { + Html::addCssClass($this->innerContainerOptions, 'container'); + } + echo Html::beginTag('div', $this->innerContainerOptions); + } + echo Html::beginTag('div', ['class' => 'navbar-header']); + if (!isset($this->containerOptions['id'])) { + $this->containerOptions['id'] = "{$this->options['id']}-collapse"; + } + echo $this->renderToggleButton(); + if ($this->brandLabel !== null) { + Html::addCssClass($this->brandOptions, 'navbar-brand'); + echo Html::a($this->brandLabel, $this->brandUrl === null ? Yii::$app->homeUrl : $this->brandUrl, $this->brandOptions); + } + echo Html::endTag('div'); + Html::addCssClass($this->containerOptions, 'collapse'); + Html::addCssClass($this->containerOptions, 'navbar-collapse'); + $options = $this->containerOptions; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options); + } - /** - * Renders the widget. - */ - public function run() - { - $tag = ArrayHelper::remove($this->containerOptions, 'tag', 'div'); - echo Html::endTag($tag); - if ($this->renderInnerContainer) { - echo Html::endTag('div'); - } - $tag = ArrayHelper::remove($this->options, 'tag', 'nav'); - echo Html::endTag($tag, $this->options); - BootstrapPluginAsset::register($this->getView()); - } + /** + * Renders the widget. + */ + public function run() + { + $tag = ArrayHelper::remove($this->containerOptions, 'tag', 'div'); + echo Html::endTag($tag); + if ($this->renderInnerContainer) { + echo Html::endTag('div'); + } + $tag = ArrayHelper::remove($this->options, 'tag', 'nav'); + echo Html::endTag($tag, $this->options); + BootstrapPluginAsset::register($this->getView()); + } - /** - * Renders collapsible toggle button. - * @return string the rendering toggle button. - */ - protected function renderToggleButton() - { - $bar = Html::tag('span', '', ['class' => 'icon-bar']); - $screenReader = "{$this->screenReaderToggleText}"; - return Html::button("{$screenReader}\n{$bar}\n{$bar}\n{$bar}", [ - 'class' => 'navbar-toggle', - 'data-toggle' => 'collapse', - 'data-target' => "#{$this->containerOptions['id']}", - ]); - } + /** + * Renders collapsible toggle button. + * @return string the rendering toggle button. + */ + protected function renderToggleButton() + { + $bar = Html::tag('span', '', ['class' => 'icon-bar']); + $screenReader = "{$this->screenReaderToggleText}"; + + return Html::button("{$screenReader}\n{$bar}\n{$bar}\n{$bar}", [ + 'class' => 'navbar-toggle', + 'data-toggle' => 'collapse', + 'data-target' => "#{$this->containerOptions['id']}", + ]); + } } diff --git a/extensions/bootstrap/Progress.php b/extensions/bootstrap/Progress.php index 33b7bb160b0..ec0827aa561 100644 --- a/extensions/bootstrap/Progress.php +++ b/extensions/bootstrap/Progress.php @@ -59,105 +59,106 @@ */ class Progress extends Widget { - /** - * @var string the button label. - */ - public $label; - /** - * @var integer the amount of progress as a percentage. - */ - public $percent = 0; - /** - * @var array the HTML attributes of the bar. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $barOptions = []; - /** - * @var array a set of bars that are stacked together to form a single progress bar. - * Each bar is an array of the following structure: - * - * ```php - * [ - * // required, the amount of progress as a percentage. - * 'percent' => 30, - * // optional, the label to be displayed on the bar - * 'label' => '30%', - * // optional, array, additional HTML attributes for the bar tag - * 'options' => [], - * ] - * ``` - */ - public $bars; + /** + * @var string the button label. + */ + public $label; + /** + * @var integer the amount of progress as a percentage. + */ + public $percent = 0; + /** + * @var array the HTML attributes of the bar. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $barOptions = []; + /** + * @var array a set of bars that are stacked together to form a single progress bar. + * Each bar is an array of the following structure: + * + * ```php + * [ + * // required, the amount of progress as a percentage. + * 'percent' => 30, + * // optional, the label to be displayed on the bar + * 'label' => '30%', + * // optional, array, additional HTML attributes for the bar tag + * 'options' => [], + * ] + * ``` + */ + public $bars; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'progress'); + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - Html::addCssClass($this->options, 'progress'); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderProgress() . "\n"; + echo Html::endTag('div') . "\n"; + BootstrapAsset::register($this->getView()); + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::beginTag('div', $this->options) . "\n"; - echo $this->renderProgress() . "\n"; - echo Html::endTag('div') . "\n"; - BootstrapAsset::register($this->getView()); - } + /** + * Renders the progress. + * @return string the rendering result. + * @throws InvalidConfigException if the "percent" option is not set in a stacked progress bar. + */ + protected function renderProgress() + { + if (empty($this->bars)) { + return $this->renderBar($this->percent, $this->label, $this->barOptions); + } + $bars = []; + foreach ($this->bars as $bar) { + $label = ArrayHelper::getValue($bar, 'label', ''); + if (!isset($bar['percent'])) { + throw new InvalidConfigException("The 'percent' option is required."); + } + $options = ArrayHelper::getValue($bar, 'options', []); + $bars[] = $this->renderBar($bar['percent'], $label, $options); + } - /** - * Renders the progress. - * @return string the rendering result. - * @throws InvalidConfigException if the "percent" option is not set in a stacked progress bar. - */ - protected function renderProgress() - { - if (empty($this->bars)) { - return $this->renderBar($this->percent, $this->label, $this->barOptions); - } - $bars = []; - foreach ($this->bars as $bar) { - $label = ArrayHelper::getValue($bar, 'label', ''); - if (!isset($bar['percent'])) { - throw new InvalidConfigException("The 'percent' option is required."); - } - $options = ArrayHelper::getValue($bar, 'options', []); - $bars[] = $this->renderBar($bar['percent'], $label, $options); - } - return implode("\n", $bars); - } + return implode("\n", $bars); + } - /** - * Generates a bar - * @param integer $percent the percentage of the bar - * @param string $label, optional, the label to display at the bar - * @param array $options the HTML attributes of the bar - * @return string the rendering result. - */ - protected function renderBar($percent, $label = '', $options = []) - { - $defaultOptions = [ - 'role' => 'progressbar', - 'aria-valuenow' => $percent, - 'aria-valuemin' => 0, - 'aria-valuemax' => 100, - 'style' => "width:{$percent}%", - ]; - $options = array_merge($defaultOptions, $options); - Html::addCssClass($options, 'progress-bar'); + /** + * Generates a bar + * @param integer $percent the percentage of the bar + * @param string $label, optional, the label to display at the bar + * @param array $options the HTML attributes of the bar + * @return string the rendering result. + */ + protected function renderBar($percent, $label = '', $options = []) + { + $defaultOptions = [ + 'role' => 'progressbar', + 'aria-valuenow' => $percent, + 'aria-valuemin' => 0, + 'aria-valuemax' => 100, + 'style' => "width:{$percent}%", + ]; + $options = array_merge($defaultOptions, $options); + Html::addCssClass($options, 'progress-bar'); - $out = Html::beginTag('div', $options); - $out .= $label; - $out .= Html::tag('span', \Yii::t('yii', '{percent}% Complete', ['percent' => $percent]), [ - 'class' => 'sr-only' - ]); - $out .= Html::endTag('div'); - return $out; - } + $out = Html::beginTag('div', $options); + $out .= $label; + $out .= Html::tag('span', \Yii::t('yii', '{percent}% Complete', ['percent' => $percent]), [ + 'class' => 'sr-only' + ]); + $out .= Html::endTag('div'); + + return $out; + } } diff --git a/extensions/bootstrap/Tabs.php b/extensions/bootstrap/Tabs.php index 6c7fd2108ef..7c7274a22fc 100644 --- a/extensions/bootstrap/Tabs.php +++ b/extensions/bootstrap/Tabs.php @@ -53,179 +53,180 @@ */ class Tabs extends Widget { - /** - * @var array list of tabs in the tabs widget. Each array element represents a single - * tab with the following structure: - * - * - label: string, required, the tab header label. - * - headerOptions: array, optional, the HTML attributes of the tab header. - * - linkOptions: array, optional, the HTML attributes of the tab header link tags. - * - content: array, required if `items` is not set. The content (HTML) of the tab pane. - * - options: array, optional, the HTML attributes of the tab pane container. - * - active: boolean, optional, whether the item tab header and pane should be visible or not. - * - items: array, optional, if not set then `content` will be required. The `items` specify a dropdown items - * configuration array. Each item can hold two extra keys, besides the above ones: - * * active: boolean, optional, whether the item tab header and pane should be visible or not. - * * content: string, required if `items` is not set. The content (HTML) of the tab pane. - * * contentOptions: optional, array, the HTML attributes of the tab content container. - */ - public $items = []; - /** - * @var array list of HTML attributes for the item container tags. This will be overwritten - * by the "options" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "div", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; - /** - * @var array list of HTML attributes for the header container tags. This will be overwritten - * by the "headerOptions" set in individual [[items]]. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $headerOptions = []; - /** - * @var array list of HTML attributes for the tab header link tags. This will be overwritten - * by the "linkOptions" set in individual [[items]]. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $linkOptions = []; - /** - * @var boolean whether the labels for header items should be HTML-encoded. - */ - public $encodeLabels = true; - /** - * @var string specifies the Bootstrap tab styling. - */ - public $navType = 'nav-tabs'; - - - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - Html::addCssClass($this->options, 'nav ' . $this->navType); - } - - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderItems(); - $this->registerPlugin('tab'); - } - - /** - * Renders tab items as specified on [[items]]. - * @return string the rendering result. - * @throws InvalidConfigException. - */ - protected function renderItems() - { - $headers = []; - $panes = []; - - if (!$this->hasActiveTab() && !empty($this->items)) { - $this->items[0]['active'] = true; - } - - foreach ($this->items as $n => $item) { - if (!isset($item['label'])) { - throw new InvalidConfigException("The 'label' option is required."); - } - $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; - $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); - $linkOptions = array_merge($this->linkOptions, ArrayHelper::getValue($item, 'linkOptions', [])); - - if (isset($item['items'])) { - $label .= ' '; - Html::addCssClass($headerOptions, 'dropdown'); - - if ($this->renderDropdown($item['items'], $panes)) { - Html::addCssClass($headerOptions, 'active'); - } - - Html::addCssClass($linkOptions, 'dropdown-toggle'); - $linkOptions['data-toggle'] = 'dropdown'; - $header = Html::a($label, "#", $linkOptions) . "\n" - . Dropdown::widget(['items' => $item['items'], 'clientOptions' => false, 'view' => $this->getView()]); - } elseif (isset($item['content'])) { - $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); - $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-tab' . $n); - - Html::addCssClass($options, 'tab-pane'); - if (ArrayHelper::remove($item, 'active')) { - Html::addCssClass($options, 'active'); - Html::addCssClass($headerOptions, 'active'); - } - $linkOptions['data-toggle'] = 'tab'; - $header = Html::a($label, '#' . $options['id'], $linkOptions); - $panes[] = Html::tag('div', $item['content'], $options); - } else { - throw new InvalidConfigException("Either the 'content' or 'items' option must be set."); - } - - $headers[] = Html::tag('li', $header, $headerOptions); - } - - return Html::tag('ul', implode("\n", $headers), $this->options) . "\n" - . Html::tag('div', implode("\n", $panes), ['class' => 'tab-content']); - } - - /** - * @return boolean if there's active tab defined - */ - protected function hasActiveTab() - { - foreach ($this->items as $item) { - if (isset($item['active']) && $item['active']===true) { - return true; - } - } - return false; - } - - /** - * Normalizes dropdown item options by removing tab specific keys `content` and `contentOptions`, and also - * configure `panes` accordingly. - * @param array $items the dropdown items configuration. - * @param array $panes the panes reference array. - * @return boolean whether any of the dropdown items is `active` or not. - * @throws InvalidConfigException - */ - protected function renderDropdown(&$items, &$panes) - { - $itemActive = false; - - foreach ($items as $n => &$item) { - if (is_string($item)) { - continue; - } - if (!isset($item['content'])) { - throw new InvalidConfigException("The 'content' option is required."); - } - - $content = ArrayHelper::remove($item, 'content'); - $options = ArrayHelper::remove($item, 'contentOptions', []); - Html::addCssClass($options, 'tab-pane'); - if (ArrayHelper::remove($item, 'active')) { - Html::addCssClass($options, 'active'); - Html::addCssClass($item['options'], 'active'); - $itemActive = true; - } - - $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-dd-tab' . $n); - $item['url'] = '#' . $options['id']; - $item['linkOptions']['data-toggle'] = 'tab'; - - $panes[] = Html::tag('div', $content, $options); - - unset($item); - } - return $itemActive; - } + /** + * @var array list of tabs in the tabs widget. Each array element represents a single + * tab with the following structure: + * + * - label: string, required, the tab header label. + * - headerOptions: array, optional, the HTML attributes of the tab header. + * - linkOptions: array, optional, the HTML attributes of the tab header link tags. + * - content: array, required if `items` is not set. The content (HTML) of the tab pane. + * - options: array, optional, the HTML attributes of the tab pane container. + * - active: boolean, optional, whether the item tab header and pane should be visible or not. + * - items: array, optional, if not set then `content` will be required. The `items` specify a dropdown items + * configuration array. Each item can hold two extra keys, besides the above ones: + * * active: boolean, optional, whether the item tab header and pane should be visible or not. + * * content: string, required if `items` is not set. The content (HTML) of the tab pane. + * * contentOptions: optional, array, the HTML attributes of the tab content container. + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * @var array list of HTML attributes for the header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array list of HTML attributes for the tab header link tags. This will be overwritten + * by the "linkOptions" set in individual [[items]]. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $linkOptions = []; + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var string specifies the Bootstrap tab styling. + */ + public $navType = 'nav-tabs'; + + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, 'nav ' . $this->navType); + } + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderItems(); + $this->registerPlugin('tab'); + } + + /** + * Renders tab items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $headers = []; + $panes = []; + + if (!$this->hasActiveTab() && !empty($this->items)) { + $this->items[0]['active'] = true; + } + + foreach ($this->items as $n => $item) { + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); + $linkOptions = array_merge($this->linkOptions, ArrayHelper::getValue($item, 'linkOptions', [])); + + if (isset($item['items'])) { + $label .= ' '; + Html::addCssClass($headerOptions, 'dropdown'); + + if ($this->renderDropdown($item['items'], $panes)) { + Html::addCssClass($headerOptions, 'active'); + } + + Html::addCssClass($linkOptions, 'dropdown-toggle'); + $linkOptions['data-toggle'] = 'dropdown'; + $header = Html::a($label, "#", $linkOptions) . "\n" + . Dropdown::widget(['items' => $item['items'], 'clientOptions' => false, 'view' => $this->getView()]); + } elseif (isset($item['content'])) { + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-tab' . $n); + + Html::addCssClass($options, 'tab-pane'); + if (ArrayHelper::remove($item, 'active')) { + Html::addCssClass($options, 'active'); + Html::addCssClass($headerOptions, 'active'); + } + $linkOptions['data-toggle'] = 'tab'; + $header = Html::a($label, '#' . $options['id'], $linkOptions); + $panes[] = Html::tag('div', $item['content'], $options); + } else { + throw new InvalidConfigException("Either the 'content' or 'items' option must be set."); + } + + $headers[] = Html::tag('li', $header, $headerOptions); + } + + return Html::tag('ul', implode("\n", $headers), $this->options) . "\n" + . Html::tag('div', implode("\n", $panes), ['class' => 'tab-content']); + } + + /** + * @return boolean if there's active tab defined + */ + protected function hasActiveTab() + { + foreach ($this->items as $item) { + if (isset($item['active']) && $item['active']===true) { + return true; + } + } + + return false; + } + + /** + * Normalizes dropdown item options by removing tab specific keys `content` and `contentOptions`, and also + * configure `panes` accordingly. + * @param array $items the dropdown items configuration. + * @param array $panes the panes reference array. + * @return boolean whether any of the dropdown items is `active` or not. + * @throws InvalidConfigException + */ + protected function renderDropdown(&$items, &$panes) + { + $itemActive = false; + + foreach ($items as $n => &$item) { + if (is_string($item)) { + continue; + } + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + + $content = ArrayHelper::remove($item, 'content'); + $options = ArrayHelper::remove($item, 'contentOptions', []); + Html::addCssClass($options, 'tab-pane'); + if (ArrayHelper::remove($item, 'active')) { + Html::addCssClass($options, 'active'); + Html::addCssClass($item['options'], 'active'); + $itemActive = true; + } + + $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-dd-tab' . $n); + $item['url'] = '#' . $options['id']; + $item['linkOptions']['data-toggle'] = 'tab'; + + $panes[] = Html::tag('div', $content, $options); + + unset($item); + } + + return $itemActive; + } } diff --git a/extensions/bootstrap/Widget.php b/extensions/bootstrap/Widget.php index e135737e8a0..2984e7a9a22 100644 --- a/extensions/bootstrap/Widget.php +++ b/extensions/bootstrap/Widget.php @@ -19,64 +19,63 @@ */ class Widget extends \yii\base\Widget { - /** - * @var array the HTML attributes for the widget container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array the options for the underlying Bootstrap JS plugin. - * Please refer to the corresponding Bootstrap plugin Web page for possible options. - * For example, [this page](http://getbootstrap.com/javascript/#modals) shows - * how to use the "Modal" plugin and the supported options (e.g. "remote"). - */ - public $clientOptions = []; - /** - * @var array the event handlers for the underlying Bootstrap JS plugin. - * Please refer to the corresponding Bootstrap plugin Web page for possible events. - * For example, [this page](http://getbootstrap.com/javascript/#modals) shows - * how to use the "Modal" plugin and the supported events (e.g. "shown"). - */ - public $clientEvents = []; + /** + * @var array the HTML attributes for the widget container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the options for the underlying Bootstrap JS plugin. + * Please refer to the corresponding Bootstrap plugin Web page for possible options. + * For example, [this page](http://getbootstrap.com/javascript/#modals) shows + * how to use the "Modal" plugin and the supported options (e.g. "remote"). + */ + public $clientOptions = []; + /** + * @var array the event handlers for the underlying Bootstrap JS plugin. + * Please refer to the corresponding Bootstrap plugin Web page for possible events. + * For example, [this page](http://getbootstrap.com/javascript/#modals) shows + * how to use the "Modal" plugin and the supported events (e.g. "shown"). + */ + public $clientEvents = []; + /** + * Initializes the widget. + * This method will register the bootstrap asset bundle. If you override this method, + * make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } - /** - * Initializes the widget. - * This method will register the bootstrap asset bundle. If you override this method, - * make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - } + /** + * Registers a specific Bootstrap plugin and the related events + * @param string $name the name of the Bootstrap plugin + */ + protected function registerPlugin($name) + { + $view = $this->getView(); - /** - * Registers a specific Bootstrap plugin and the related events - * @param string $name the name of the Bootstrap plugin - */ - protected function registerPlugin($name) - { - $view = $this->getView(); + BootstrapPluginAsset::register($view); - BootstrapPluginAsset::register($view); + $id = $this->options['id']; - $id = $this->options['id']; + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').$name($options);"; + $view->registerJs($js); + } - if ($this->clientOptions !== false) { - $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); - $js = "jQuery('#$id').$name($options);"; - $view->registerJs($js); - } - - if (!empty($this->clientEvents)) { - $js = []; - foreach ($this->clientEvents as $event => $handler) { - $js[] = "jQuery('#$id').on('$event', $handler);"; - } - $view->registerJs(implode("\n", $js)); - } - } + if (!empty($this->clientEvents)) { + $js = []; + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } } diff --git a/extensions/codeception/BasePage.php b/extensions/codeception/BasePage.php index 145e0c32250..32a12a38d7b 100644 --- a/extensions/codeception/BasePage.php +++ b/extensions/codeception/BasePage.php @@ -16,56 +16,58 @@ */ abstract class BasePage extends Component { - /** - * @var string|array the route (controller ID and action ID, e.g. `site/about`) to this page. - * Use array to represent a route with GET parameters. The first element of the array represents - * the route and the rest of the name-value pairs are treated as GET parameters, e.g. `array('site/page', 'name' => 'about')`. - */ - public $route; - /** - * @var \Codeception\AbstractGuy the testing guy object - */ - protected $guy; + /** + * @var string|array the route (controller ID and action ID, e.g. `site/about`) to this page. + * Use array to represent a route with GET parameters. The first element of the array represents + * the route and the rest of the name-value pairs are treated as GET parameters, e.g. `array('site/page', 'name' => 'about')`. + */ + public $route; + /** + * @var \Codeception\AbstractGuy the testing guy object + */ + protected $guy; - /** - * Constructor. - * @param \Codeception\AbstractGuy the testing guy object - */ - public function __construct($I) - { - $this->guy = $I; - } + /** + * Constructor. + * @param \Codeception\AbstractGuy the testing guy object + */ + public function __construct($I) + { + $this->guy = $I; + } - /** - * Returns the URL to this page. - * The URL will be returned by calling the URL manager of the application - * with [[route]] and the provided parameters. - * @param array $params the GET parameters for creating the URL - * @return string the URL to this page - * @throws InvalidConfigException if [[route]] is not set or invalid - */ - public function getUrl($params = []) - { - if (is_string($this->route)) { - $params[0] = $this->route; - return Yii::$app->getUrlManager()->createUrl($params); - } elseif (is_array($this->route) && isset($this->route[0])) { - return Yii::$app->getUrlManager()->createUrl(array_merge($this->route, $params)); - } else { - throw new InvalidConfigException('The "route" property must be set.'); - } - } + /** + * Returns the URL to this page. + * The URL will be returned by calling the URL manager of the application + * with [[route]] and the provided parameters. + * @param array $params the GET parameters for creating the URL + * @return string the URL to this page + * @throws InvalidConfigException if [[route]] is not set or invalid + */ + public function getUrl($params = []) + { + if (is_string($this->route)) { + $params[0] = $this->route; - /** - * Creates a page instance and sets the test guy to use [[url]]. - * @param \Codeception\AbstractGuy $I the test guy instance - * @param array $params the GET parameters to be used to generate [[url]] - * @return static the page instance - */ - public static function openBy($I, $params = []) - { - $page = new static($I); - $I->amOnPage($page->getUrl($params)); - return $page; - } + return Yii::$app->getUrlManager()->createUrl($params); + } elseif (is_array($this->route) && isset($this->route[0])) { + return Yii::$app->getUrlManager()->createUrl(array_merge($this->route, $params)); + } else { + throw new InvalidConfigException('The "route" property must be set.'); + } + } + + /** + * Creates a page instance and sets the test guy to use [[url]]. + * @param \Codeception\AbstractGuy $I the test guy instance + * @param array $params the GET parameters to be used to generate [[url]] + * @return static the page instance + */ + public static function openBy($I, $params = []) + { + $page = new static($I); + $I->amOnPage($page->getUrl($params)); + + return $page; + } } diff --git a/extensions/codeception/DbTestCase.php b/extensions/codeception/DbTestCase.php index ef6a3790cad..e962e7ab793 100644 --- a/extensions/codeception/DbTestCase.php +++ b/extensions/codeception/DbTestCase.php @@ -15,13 +15,13 @@ */ class DbTestCase extends TestCase { - /** - * @inheritdoc - */ - public function globalFixtures() - { - return [ - InitDbFixture::className(), - ]; - } + /** + * @inheritdoc + */ + public function globalFixtures() + { + return [ + InitDbFixture::className(), + ]; + } } diff --git a/extensions/codeception/TestCase.php b/extensions/codeception/TestCase.php index 3ba95265f86..de0f1a7b587 100644 --- a/extensions/codeception/TestCase.php +++ b/extensions/codeception/TestCase.php @@ -18,107 +18,108 @@ */ class TestCase extends Test { - use FixtureTrait; + use FixtureTrait; - /** - * @var array|string the application configuration that will be used for creating an application instance for each test. - * You can use a string to represent the file path or path alias of a configuration file. - * The application configuration array may contain an optional `class` element which specifies the class - * name of the application instance to be created. By default, a [[\yii\web\Application]] instance will be created. - */ - public $appConfig = '@tests/unit/_config.php'; + /** + * @var array|string the application configuration that will be used for creating an application instance for each test. + * You can use a string to represent the file path or path alias of a configuration file. + * The application configuration array may contain an optional `class` element which specifies the class + * name of the application instance to be created. By default, a [[\yii\web\Application]] instance will be created. + */ + public $appConfig = '@tests/unit/_config.php'; - /** - * Returns the value of an object property. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `$value = $object->property;`. - * @param string $name the property name - * @return mixed the property value - * @throws UnknownPropertyException if the property is not defined - */ - public function __get($name) - { - $fixture = $this->getFixture($name); - if ($fixture !== null) { - return $fixture; - } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); - } - } + /** + * Returns the value of an object property. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `$value = $object->property;`. + * @param string $name the property name + * @return mixed the property value + * @throws UnknownPropertyException if the property is not defined + */ + public function __get($name) + { + $fixture = $this->getFixture($name); + if ($fixture !== null) { + return $fixture; + } else { + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); + } + } - /** - * Calls the named method which is not a class method. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when an unknown method is being invoked. - * @param string $name the method name - * @param array $params method parameters - * @throws UnknownMethodException when calling unknown method - * @return mixed the method return value - */ - public function __call($name, $params) - { - $fixture = $this->getFixture($name); - if ($fixture instanceof ActiveFixture) { - return $fixture->getModel(reset($params)); - } else { - throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); - } - } + /** + * Calls the named method which is not a class method. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when an unknown method is being invoked. + * @param string $name the method name + * @param array $params method parameters + * @throws UnknownMethodException when calling unknown method + * @return mixed the method return value + */ + public function __call($name, $params) + { + $fixture = $this->getFixture($name); + if ($fixture instanceof ActiveFixture) { + return $fixture->getModel(reset($params)); + } else { + throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); + } + } - /** - * @inheritdoc - */ - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $this->unloadFixtures(); - $this->loadFixtures(); - } + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->unloadFixtures(); + $this->loadFixtures(); + } - /** - * @inheritdoc - */ - protected function tearDown() - { - $this->destroyApplication(); - parent::tearDown(); - } + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->destroyApplication(); + parent::tearDown(); + } - /** - * Mocks up the application instance. - * @param array $config the configuration that should be used to generate the application instance. - * If null, [[appConfig]] will be used. - * @return \yii\web\Application|\yii\console\Application the application instance - * @throws InvalidConfigException if the application configuration is invalid - */ - protected function mockApplication($config = null) - { - $config = $config === null ? $this->appConfig : $config; - if (is_string($config)) { - $configFile = Yii::getAlias($config); - if (!is_file($configFile)) { - throw new InvalidConfigException("The application configuration file does not exist: $config"); - } - $config = require($configFile); - } - if (is_array($config)) { - if (!isset($config['class'])) { - $config['class'] = 'yii\web\Application'; - } - return Yii::createObject($config); - } else { - throw new InvalidConfigException('Please provide a configuration array to mock up an application.'); - } - } + /** + * Mocks up the application instance. + * @param array $config the configuration that should be used to generate the application instance. + * If null, [[appConfig]] will be used. + * @return \yii\web\Application|\yii\console\Application the application instance + * @throws InvalidConfigException if the application configuration is invalid + */ + protected function mockApplication($config = null) + { + $config = $config === null ? $this->appConfig : $config; + if (is_string($config)) { + $configFile = Yii::getAlias($config); + if (!is_file($configFile)) { + throw new InvalidConfigException("The application configuration file does not exist: $config"); + } + $config = require($configFile); + } + if (is_array($config)) { + if (!isset($config['class'])) { + $config['class'] = 'yii\web\Application'; + } - /** - * Destroys the application instance created by [[mockApplication]]. - */ - protected function destroyApplication() - { - Yii::$app = null; - } + return Yii::createObject($config); + } else { + throw new InvalidConfigException('Please provide a configuration array to mock up an application.'); + } + } + + /** + * Destroys the application instance created by [[mockApplication]]. + */ + protected function destroyApplication() + { + Yii::$app = null; + } } diff --git a/extensions/composer/Installer.php b/extensions/composer/Installer.php index 81cc2677282..e597101a4d1 100644 --- a/extensions/composer/Installer.php +++ b/extensions/composer/Installer.php @@ -19,181 +19,181 @@ */ class Installer extends LibraryInstaller { - const EXTRA_BOOTSTRAP = 'bootstrap'; - const EXTRA_WRITABLE = 'writable'; - const EXTRA_EXECUTABLE = 'executable'; + const EXTRA_BOOTSTRAP = 'bootstrap'; + const EXTRA_WRITABLE = 'writable'; + const EXTRA_EXECUTABLE = 'executable'; - const EXTENSION_FILE = 'yiisoft/extensions.php'; + const EXTENSION_FILE = 'yiisoft/extensions.php'; - /** - * @inheritdoc - */ - public function supports($packageType) - { - return $packageType === 'yii2-extension'; - } + /** + * @inheritdoc + */ + public function supports($packageType) + { + return $packageType === 'yii2-extension'; + } - /** - * @inheritdoc - */ - public function install(InstalledRepositoryInterface $repo, PackageInterface $package) - { - // install the package the normal composer way - parent::install($repo, $package); - // add the package to yiisoft/extensions.php - $this->addPackage($package); - // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does - if ($package->getName() == 'yiisoft/yii2-dev') { - $this->linkBaseYiiFiles(); - } - } + /** + * @inheritdoc + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + // install the package the normal composer way + parent::install($repo, $package); + // add the package to yiisoft/extensions.php + $this->addPackage($package); + // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does + if ($package->getName() == 'yiisoft/yii2-dev') { + $this->linkBaseYiiFiles(); + } + } - /** - * @inheritdoc - */ - public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) - { - parent::update($repo, $initial, $target); - $this->removePackage($initial); - $this->addPackage($target); - // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does - if ($initial->getName() == 'yiisoft/yii2-dev') { - $this->linkBaseYiiFiles(); - } - } + /** + * @inheritdoc + */ + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + $this->removePackage($initial); + $this->addPackage($target); + // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does + if ($initial->getName() == 'yiisoft/yii2-dev') { + $this->linkBaseYiiFiles(); + } + } - /** - * @inheritdoc - */ - public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) - { - // uninstall the package the normal composer way - parent::uninstall($repo, $package); - // remove the package from yiisoft/extensions.php - $this->removePackage($package); - // remove links for Yii.php - if ($package->getName() == 'yiisoft/yii2-dev') { - $this->removeBaseYiiFiles(); - } - } + /** + * @inheritdoc + */ + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + // uninstall the package the normal composer way + parent::uninstall($repo, $package); + // remove the package from yiisoft/extensions.php + $this->removePackage($package); + // remove links for Yii.php + if ($package->getName() == 'yiisoft/yii2-dev') { + $this->removeBaseYiiFiles(); + } + } - protected function addPackage(PackageInterface $package) - { - $extension = [ - 'name' => $package->getName(), - 'version' => $package->getVersion(), - ]; + protected function addPackage(PackageInterface $package) + { + $extension = [ + 'name' => $package->getName(), + 'version' => $package->getVersion(), + ]; - $alias = $this->generateDefaultAlias($package); - if (!empty($alias)) { - $extension['alias'] = $alias; - } - $extra = $package->getExtra(); - if (isset($extra[self::EXTRA_BOOTSTRAP]) && is_string($extra[self::EXTRA_BOOTSTRAP])) { - $extension['bootstrap'] = $extra[self::EXTRA_BOOTSTRAP]; - } + $alias = $this->generateDefaultAlias($package); + if (!empty($alias)) { + $extension['alias'] = $alias; + } + $extra = $package->getExtra(); + if (isset($extra[self::EXTRA_BOOTSTRAP]) && is_string($extra[self::EXTRA_BOOTSTRAP])) { + $extension['bootstrap'] = $extra[self::EXTRA_BOOTSTRAP]; + } - $extensions = $this->loadExtensions(); - $extensions[$package->getName()] = $extension; - $this->saveExtensions($extensions); - } + $extensions = $this->loadExtensions(); + $extensions[$package->getName()] = $extension; + $this->saveExtensions($extensions); + } - protected function generateDefaultAlias(PackageInterface $package) - { - $fs = new Filesystem; - $vendorDir = $fs->normalizePath($this->vendorDir); - $autoload = $package->getAutoload(); + protected function generateDefaultAlias(PackageInterface $package) + { + $fs = new Filesystem; + $vendorDir = $fs->normalizePath($this->vendorDir); + $autoload = $package->getAutoload(); - $aliases = []; + $aliases = []; - if (!empty($autoload['psr-0'])) { - foreach ($autoload['psr-0'] as $name => $path) { - $name = str_replace('\\', '/', trim($name, '\\')); - if (!$fs->isAbsolutePath($path)) { - $path = $this->vendorDir . '/' . $package->getName() . '/' . $path; - } - $path = $fs->normalizePath($path); - if (strpos($path . '/', $vendorDir . '/') === 0) { - $aliases["@$name"] = '' . substr($path, strlen($vendorDir)) . '/' . $name; - } else { - $aliases["@$name"] = $path . '/' . $name; - } - } - } + if (!empty($autoload['psr-0'])) { + foreach ($autoload['psr-0'] as $name => $path) { + $name = str_replace('\\', '/', trim($name, '\\')); + if (!$fs->isAbsolutePath($path)) { + $path = $this->vendorDir . '/' . $package->getName() . '/' . $path; + } + $path = $fs->normalizePath($path); + if (strpos($path . '/', $vendorDir . '/') === 0) { + $aliases["@$name"] = '' . substr($path, strlen($vendorDir)) . '/' . $name; + } else { + $aliases["@$name"] = $path . '/' . $name; + } + } + } - if (!empty($autoload['psr-4'])) { - foreach ($autoload['psr-4'] as $name => $path) { - $name = str_replace('\\', '/', trim($name, '\\')); - if (!$fs->isAbsolutePath($path)) { - $path = $this->vendorDir . '/' . $package->getName() . '/' . $path; - } - $path = $fs->normalizePath($path); - if (strpos($path . '/', $vendorDir . '/') === 0) { - $aliases["@$name"] = '' . substr($path, strlen($vendorDir)); - } else { - $aliases["@$name"] = $path; - } - } - } + if (!empty($autoload['psr-4'])) { + foreach ($autoload['psr-4'] as $name => $path) { + $name = str_replace('\\', '/', trim($name, '\\')); + if (!$fs->isAbsolutePath($path)) { + $path = $this->vendorDir . '/' . $package->getName() . '/' . $path; + } + $path = $fs->normalizePath($path); + if (strpos($path . '/', $vendorDir . '/') === 0) { + $aliases["@$name"] = '' . substr($path, strlen($vendorDir)); + } else { + $aliases["@$name"] = $path; + } + } + } - return $aliases; - } + return $aliases; + } - protected function removePackage(PackageInterface $package) - { - $packages = $this->loadExtensions(); - unset($packages[$package->getName()]); - $this->saveExtensions($packages); - } + protected function removePackage(PackageInterface $package) + { + $packages = $this->loadExtensions(); + unset($packages[$package->getName()]); + $this->saveExtensions($packages); + } - protected function loadExtensions() - { - $file = $this->vendorDir . '/' . self::EXTENSION_FILE; - if (!is_file($file)) { - return []; - } - // invalidate opcache of extensions.php if exists - if (function_exists('opcache_invalidate')) { - opcache_invalidate($file, true); - } - $extensions = require($file); + protected function loadExtensions() + { + $file = $this->vendorDir . '/' . self::EXTENSION_FILE; + if (!is_file($file)) { + return []; + } + // invalidate opcache of extensions.php if exists + if (function_exists('opcache_invalidate')) { + opcache_invalidate($file, true); + } + $extensions = require($file); - $vendorDir = str_replace('\\', '/', $this->vendorDir); - $n = strlen($vendorDir); + $vendorDir = str_replace('\\', '/', $this->vendorDir); + $n = strlen($vendorDir); - foreach ($extensions as &$extension) { - if (isset($extension['alias'])) { - foreach ($extension['alias'] as $alias => $path) { - $path = str_replace('\\', '/', $path); - if (strpos($path . '/', $vendorDir . '/') === 0) { - $extension['alias'][$alias] = '' . substr($path, $n); - } - } - } - } + foreach ($extensions as &$extension) { + if (isset($extension['alias'])) { + foreach ($extension['alias'] as $alias => $path) { + $path = str_replace('\\', '/', $path); + if (strpos($path . '/', $vendorDir . '/') === 0) { + $extension['alias'][$alias] = '' . substr($path, $n); + } + } + } + } - return $extensions; - } + return $extensions; + } - protected function saveExtensions(array $extensions) - { - $file = $this->vendorDir . '/' . self::EXTENSION_FILE; - $array = str_replace("'", '$vendorDir . \'', var_export($extensions, true)); - file_put_contents($file, "vendorDir . '/' . self::EXTENSION_FILE; + $array = str_replace("'", '$vendorDir . \'', var_export($extensions, true)); + file_put_contents($file, "vendorDir . '/yiisoft/yii2'; - if (!file_exists($yiiDir)) { - mkdir($yiiDir, 0777, true); - } - foreach (['Yii.php', 'BaseYii.php', 'classes.php'] as $file) { - file_put_contents($yiiDir . '/' . $file, <<vendorDir . '/yiisoft/yii2'; + if (!file_exists($yiiDir)) { + mkdir($yiiDir, 0777, true); + } + foreach (['Yii.php', 'BaseYii.php', 'classes.php'] as $file) { + file_put_contents($yiiDir . '/' . $file, <<vendorDir . '/yiisoft/yii2'; - foreach (['Yii.php', 'BaseYii.php', 'classes.php'] as $file) { - if (file_exists($yiiDir . '/' . $file)) { - unlink($yiiDir . '/' . $file); - } - } - if (file_exists($yiiDir)) { - rmdir($yiiDir); - } - } + protected function removeBaseYiiFiles() + { + $yiiDir = $this->vendorDir . '/yiisoft/yii2'; + foreach (['Yii.php', 'BaseYii.php', 'classes.php'] as $file) { + if (file_exists($yiiDir . '/' . $file)) { + unlink($yiiDir . '/' . $file); + } + } + if (file_exists($yiiDir)) { + rmdir($yiiDir); + } + } - /** - * Sets the correct permission for the files and directories listed in the extra section. - * @param CommandEvent $event - */ - public static function setPermission($event) - { - $options = array_merge([ - self::EXTRA_WRITABLE => [], - self::EXTRA_EXECUTABLE => [], - ], $event->getComposer()->getPackage()->getExtra()); + /** + * Sets the correct permission for the files and directories listed in the extra section. + * @param CommandEvent $event + */ + public static function setPermission($event) + { + $options = array_merge([ + self::EXTRA_WRITABLE => [], + self::EXTRA_EXECUTABLE => [], + ], $event->getComposer()->getPackage()->getExtra()); - foreach ((array)$options[self::EXTRA_WRITABLE] as $path) { - echo "Setting writable: $path ..."; - if (is_dir($path)) { - chmod($path, 0777); - echo "done\n"; - } else { - echo "The directory was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path; - return; - } - } + foreach ((array) $options[self::EXTRA_WRITABLE] as $path) { + echo "Setting writable: $path ..."; + if (is_dir($path)) { + chmod($path, 0777); + echo "done\n"; + } else { + echo "The directory was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path; - foreach ((array)$options[self::EXTRA_EXECUTABLE] as $path) { - echo "Setting executable: $path ..."; - if (is_file($path)) { - chmod($path, 0755); - echo "done\n"; - } else { - echo "\n\tThe file was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path . "\n"; - return; - } - } - } + return; + } + } + + foreach ((array) $options[self::EXTRA_EXECUTABLE] as $path) { + echo "Setting executable: $path ..."; + if (is_file($path)) { + chmod($path, 0755); + echo "done\n"; + } else { + echo "\n\tThe file was not found: " . getcwd() . DIRECTORY_SEPARATOR . $path . "\n"; + + return; + } + } + } } diff --git a/extensions/composer/Plugin.php b/extensions/composer/Plugin.php index 1111738e32a..1f08d53f6b3 100644 --- a/extensions/composer/Plugin.php +++ b/extensions/composer/Plugin.php @@ -19,17 +19,17 @@ */ class Plugin implements PluginInterface { - /** - * @inheritdoc - */ - public function activate(Composer $composer, IOInterface $io) - { - $installer = new Installer($io, $composer); - $composer->getInstallationManager()->addInstaller($installer); - $file = rtrim($composer->getConfig()->get('vendor-dir'), '/') . '/yiisoft/extensions.php'; - if (!is_file($file)) { - @mkdir(dirname($file)); - file_put_contents($file, "getInstallationManager()->addInstaller($installer); + $file = rtrim($composer->getConfig()->get('vendor-dir'), '/') . '/yiisoft/extensions.php'; + if (!is_file($file)) { + @mkdir(dirname($file)); + file_put_contents($file, "module = $module; - $this->tag = uniqid(); - } - - /** - * Exports log messages to a specific destination. - * Child classes must implement this method. - */ - public function export() - { - $path = $this->module->dataPath; - if (!is_dir($path)) { - mkdir($path); - } - - $summary = $this->collectSummary(); - $dataFile = "$path/{$this->tag}.data"; - $data = []; - foreach ($this->module->panels as $id => $panel) { - $data[$id] = $panel->save(); - } - $data['summary'] = $summary; - file_put_contents($dataFile, serialize($data)); - - $indexFile = "$path/index.data"; - $this->updateIndexFile($indexFile, $summary); - } - - /** - * Updates index file with summary log data - * - * @param string $indexFile path to index file - * @param array $summary summary log data - * @throws \yii\base\InvalidConfigException - */ - private function updateIndexFile($indexFile, $summary) - { - touch($indexFile); - if (($fp = @fopen($indexFile, 'r+')) === false) { - throw new InvalidConfigException("Unable to open debug data index file: $indexFile"); - } - @flock($fp, LOCK_EX); - $manifest = ''; - while (($buffer = fgets($fp)) !== false) { - $manifest .= $buffer; - } - if (!feof($fp) || empty($manifest)) { - // error while reading index data, ignore and create new - $manifest = []; - } else { - $manifest = unserialize($manifest); - } - - $manifest[$this->tag] = $summary; - $this->gc($manifest); - - ftruncate($fp, 0); - rewind($fp); - fwrite($fp, serialize($manifest)); - - @flock($fp, LOCK_UN); - @fclose($fp); - } - - /** - * Processes the given log messages. - * This method will filter the given messages with [[levels]] and [[categories]]. - * And if requested, it will also export the filtering result to specific medium (e.g. email). - * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure - * of each message. - * @param boolean $final whether this method is called at the end of the current application - */ - public function collect($messages, $final) - { - $this->messages = array_merge($this->messages, $messages); - if ($final) { - $this->export($this->messages); - } - } - - protected function gc(&$manifest) - { - if (count($manifest) > $this->module->historySize + 10) { - $n = count($manifest) - $this->module->historySize; - foreach (array_keys($manifest) as $tag) { - $file = $this->module->dataPath . "/$tag.data"; - @unlink($file); - unset($manifest[$tag]); - if (--$n <= 0) { - break; - } - } - } - } - - /** - * Collects summary data of current request. - * @return array - */ - protected function collectSummary() - { - $request = Yii::$app->getRequest(); - $response = Yii::$app->getResponse(); - $summary = [ - 'tag' => $this->tag, - 'url' => $request->getAbsoluteUrl(), - 'ajax' => $request->getIsAjax(), - 'method' => $request->getMethod(), - 'ip' => $request->getUserIP(), - 'time' => time(), - 'statusCode' => $response->statusCode, - 'sqlCount' => $this->getSqlTotalCount(), - ]; - - if (isset($this->module->panels['mail'])) { - $summary['mailCount'] = count($this->module->panels['mail']->getMessages()); - } - - return $summary; - } - - /** - * Returns total sql count executed in current request. If database panel is not configured - * returns 0. - * @return integer - */ - protected function getSqlTotalCount() - { - if (!isset($this->module->panels['db'])) { - return 0; - } - $profileLogs = $this->module->panels['db']->getProfileLogs(); - - # / 2 because messages are in couple (begin/end) - return count($profileLogs) / 2; - } + /** + * @var Module + */ + public $module; + public $tag; + + /** + * @param \yii\debug\Module $module + * @param array $config + */ + public function __construct($module, $config = []) + { + parent::__construct($config); + $this->module = $module; + $this->tag = uniqid(); + } + + /** + * Exports log messages to a specific destination. + * Child classes must implement this method. + */ + public function export() + { + $path = $this->module->dataPath; + if (!is_dir($path)) { + mkdir($path); + } + + $summary = $this->collectSummary(); + $dataFile = "$path/{$this->tag}.data"; + $data = []; + foreach ($this->module->panels as $id => $panel) { + $data[$id] = $panel->save(); + } + $data['summary'] = $summary; + file_put_contents($dataFile, serialize($data)); + + $indexFile = "$path/index.data"; + $this->updateIndexFile($indexFile, $summary); + } + + /** + * Updates index file with summary log data + * + * @param string $indexFile path to index file + * @param array $summary summary log data + * @throws \yii\base\InvalidConfigException + */ + private function updateIndexFile($indexFile, $summary) + { + touch($indexFile); + if (($fp = @fopen($indexFile, 'r+')) === false) { + throw new InvalidConfigException("Unable to open debug data index file: $indexFile"); + } + @flock($fp, LOCK_EX); + $manifest = ''; + while (($buffer = fgets($fp)) !== false) { + $manifest .= $buffer; + } + if (!feof($fp) || empty($manifest)) { + // error while reading index data, ignore and create new + $manifest = []; + } else { + $manifest = unserialize($manifest); + } + + $manifest[$this->tag] = $summary; + $this->gc($manifest); + + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, serialize($manifest)); + + @flock($fp, LOCK_UN); + @fclose($fp); + } + + /** + * Processes the given log messages. + * This method will filter the given messages with [[levels]] and [[categories]]. + * And if requested, it will also export the filtering result to specific medium (e.g. email). + * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure + * of each message. + * @param boolean $final whether this method is called at the end of the current application + */ + public function collect($messages, $final) + { + $this->messages = array_merge($this->messages, $messages); + if ($final) { + $this->export($this->messages); + } + } + + protected function gc(&$manifest) + { + if (count($manifest) > $this->module->historySize + 10) { + $n = count($manifest) - $this->module->historySize; + foreach (array_keys($manifest) as $tag) { + $file = $this->module->dataPath . "/$tag.data"; + @unlink($file); + unset($manifest[$tag]); + if (--$n <= 0) { + break; + } + } + } + } + + /** + * Collects summary data of current request. + * @return array + */ + protected function collectSummary() + { + $request = Yii::$app->getRequest(); + $response = Yii::$app->getResponse(); + $summary = [ + 'tag' => $this->tag, + 'url' => $request->getAbsoluteUrl(), + 'ajax' => $request->getIsAjax(), + 'method' => $request->getMethod(), + 'ip' => $request->getUserIP(), + 'time' => time(), + 'statusCode' => $response->statusCode, + 'sqlCount' => $this->getSqlTotalCount(), + ]; + + if (isset($this->module->panels['mail'])) { + $summary['mailCount'] = count($this->module->panels['mail']->getMessages()); + } + + return $summary; + } + + /** + * Returns total sql count executed in current request. If database panel is not configured + * returns 0. + * @return integer + */ + protected function getSqlTotalCount() + { + if (!isset($this->module->panels['db'])) { + return 0; + } + $profileLogs = $this->module->panels['db']->getProfileLogs(); + + # / 2 because messages are in couple (begin/end) + + return count($profileLogs) / 2; + } } diff --git a/extensions/debug/Module.php b/extensions/debug/Module.php index b6d62304cb7..a2c534c8561 100644 --- a/extensions/debug/Module.php +++ b/extensions/debug/Module.php @@ -20,148 +20,149 @@ */ class Module extends \yii\base\Module { - /** - * @var array the list of IPs that are allowed to access this module. - * Each array element represents a single IP filter which can be either an IP address - * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment. - * The default value is `['127.0.0.1', '::1']`, which means the module can only be accessed - * by localhost. - */ - public $allowedIPs = ['127.0.0.1', '::1']; - /** - * @var string the namespace that controller classes are in. - */ - public $controllerNamespace = 'yii\debug\controllers'; - /** - * @var LogTarget - */ - public $logTarget; - /** - * @var array list of debug panels. The array keys are the panel IDs, and values are the corresponding - * panel class names or configuration arrays. This will be merged with [[corePanels()]]. - * You may reconfigure a core panel via this property by using the same panel ID. - * You may also disable a core panel by setting it to be false in this property. - */ - public $panels = []; - /** - * @var string the directory storing the debugger data files. This can be specified using a path alias. - */ - public $dataPath = '@runtime/debug'; - /** - * @var integer the maximum number of debug data files to keep. If there are more files generated, - * the oldest ones will be removed. - */ - public $historySize = 50; + /** + * @var array the list of IPs that are allowed to access this module. + * Each array element represents a single IP filter which can be either an IP address + * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment. + * The default value is `['127.0.0.1', '::1']`, which means the module can only be accessed + * by localhost. + */ + public $allowedIPs = ['127.0.0.1', '::1']; + /** + * @var string the namespace that controller classes are in. + */ + public $controllerNamespace = 'yii\debug\controllers'; + /** + * @var LogTarget + */ + public $logTarget; + /** + * @var array list of debug panels. The array keys are the panel IDs, and values are the corresponding + * panel class names or configuration arrays. This will be merged with [[corePanels()]]. + * You may reconfigure a core panel via this property by using the same panel ID. + * You may also disable a core panel by setting it to be false in this property. + */ + public $panels = []; + /** + * @var string the directory storing the debugger data files. This can be specified using a path alias. + */ + public $dataPath = '@runtime/debug'; + /** + * @var integer the maximum number of debug data files to keep. If there are more files generated, + * the oldest ones will be removed. + */ + public $historySize = 50; - /** - * Returns Yii logo ready to use in `on(Application::EVENT_BEFORE_REQUEST, function () { - Yii::$app->getView()->on(View::EVENT_END_BODY, [$this, 'renderToolbar']); - }); + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->dataPath = Yii::getAlias($this->dataPath); + $this->logTarget = Yii::$app->getLog()->targets['debug'] = new LogTarget($this); + // do not initialize view component before application is ready (needed when debug in preload) + Yii::$app->on(Application::EVENT_BEFORE_REQUEST, function () { + Yii::$app->getView()->on(View::EVENT_END_BODY, [$this, 'renderToolbar']); + }); - // merge custom panels and core panels so that they are ordered mainly by custom panels - if (empty($this->panels)) { - $this->panels = $this->corePanels(); - } else { - $corePanels = $this->corePanels(); - foreach ($corePanels as $id => $config) { - if (isset($this->panels[$id])) { - unset($corePanels[$id]); - } - } - $this->panels = array_filter(array_merge($corePanels, $this->panels)); - } + // merge custom panels and core panels so that they are ordered mainly by custom panels + if (empty($this->panels)) { + $this->panels = $this->corePanels(); + } else { + $corePanels = $this->corePanels(); + foreach ($corePanels as $id => $config) { + if (isset($this->panels[$id])) { + unset($corePanels[$id]); + } + } + $this->panels = array_filter(array_merge($corePanels, $this->panels)); + } - foreach ($this->panels as $id => $config) { - $config['module'] = $this; - $config['id'] = $id; - $this->panels[$id] = Yii::createObject($config); - } - } + foreach ($this->panels as $id => $config) { + $config['module'] = $this; + $config['id'] = $id; + $this->panels[$id] = Yii::createObject($config); + } + } - /** - * @inheritdoc - */ - public function beforeAction($action) - { - Yii::$app->getView()->off(View::EVENT_END_BODY, [$this, 'renderToolbar']); - unset(Yii::$app->getLog()->targets['debug']); - $this->logTarget = null; + /** + * @inheritdoc + */ + public function beforeAction($action) + { + Yii::$app->getView()->off(View::EVENT_END_BODY, [$this, 'renderToolbar']); + unset(Yii::$app->getLog()->targets['debug']); + $this->logTarget = null; - if ($this->checkAccess()) { - return parent::beforeAction($action); - } elseif ($action->id === 'toolbar') { - return false; - } else { - throw new ForbiddenHttpException('You are not allowed to access this page.'); - } - } + if ($this->checkAccess()) { + return parent::beforeAction($action); + } elseif ($action->id === 'toolbar') { + return false; + } else { + throw new ForbiddenHttpException('You are not allowed to access this page.'); + } + } - /** - * Renders mini-toolbar at the end of page body. - * - * @param \yii\base\Event $event - */ - public function renderToolbar($event) - { - if (!$this->checkAccess() || Yii::$app->getRequest()->getIsAjax()) { - return; - } - $url = Yii::$app->getUrlManager()->createUrl([$this->id . '/default/toolbar', - 'tag' => $this->logTarget->tag, - ]); - echo ''; - /** @var View $view */ - $view = $event->sender; - echo ''; - echo ''; - } + /** + * Renders mini-toolbar at the end of page body. + * + * @param \yii\base\Event $event + */ + public function renderToolbar($event) + { + if (!$this->checkAccess() || Yii::$app->getRequest()->getIsAjax()) { + return; + } + $url = Yii::$app->getUrlManager()->createUrl([$this->id . '/default/toolbar', + 'tag' => $this->logTarget->tag, + ]); + echo ''; + /** @var View $view */ + $view = $event->sender; + echo ''; + echo ''; + } - /** - * Checks if current user is allowed to access the module - * @return boolean if access is granted - */ - protected function checkAccess() - { - $ip = Yii::$app->getRequest()->getUserIP(); - foreach ($this->allowedIPs as $filter) { - if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) { - return true; - } - } - Yii::warning('Access to debugger is denied due to IP address restriction. The requested IP is ' . $ip, __METHOD__); - return false; - } + /** + * Checks if current user is allowed to access the module + * @return boolean if access is granted + */ + protected function checkAccess() + { + $ip = Yii::$app->getRequest()->getUserIP(); + foreach ($this->allowedIPs as $filter) { + if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) { + return true; + } + } + Yii::warning('Access to debugger is denied due to IP address restriction. The requested IP is ' . $ip, __METHOD__); - /** - * @return array default set of panels - */ - protected function corePanels() - { - return [ - 'config' => ['class' => 'yii\debug\panels\ConfigPanel'], - 'request' => ['class' => 'yii\debug\panels\RequestPanel'], - 'log' => ['class' => 'yii\debug\panels\LogPanel'], - 'profiling' => ['class' => 'yii\debug\panels\ProfilingPanel'], - 'db' => ['class' => 'yii\debug\panels\DbPanel'], - 'mail' => ['class' => 'yii\debug\panels\MailPanel'], - ]; - } + return false; + } + + /** + * @return array default set of panels + */ + protected function corePanels() + { + return [ + 'config' => ['class' => 'yii\debug\panels\ConfigPanel'], + 'request' => ['class' => 'yii\debug\panels\RequestPanel'], + 'log' => ['class' => 'yii\debug\panels\LogPanel'], + 'profiling' => ['class' => 'yii\debug\panels\ProfilingPanel'], + 'db' => ['class' => 'yii\debug\panels\DbPanel'], + 'mail' => ['class' => 'yii\debug\panels\MailPanel'], + ]; + } } diff --git a/extensions/debug/Panel.php b/extensions/debug/Panel.php index 0f9e673c9d5..2e4b2b346b9 100644 --- a/extensions/debug/Panel.php +++ b/extensions/debug/Panel.php @@ -24,73 +24,73 @@ */ class Panel extends Component { - public $id; - public $tag; - /** - * @var Module - */ - public $module; - public $data; - /** - * @var array array of actions to add to the debug modules default controller. - * This array will be merged with all other panels actions property. - * See [[\yii\base\Controller::actions()]] for the format. - */ - public $actions = []; + public $id; + public $tag; + /** + * @var Module + */ + public $module; + public $data; + /** + * @var array array of actions to add to the debug modules default controller. + * This array will be merged with all other panels actions property. + * See [[\yii\base\Controller::actions()]] for the format. + */ + public $actions = []; - /** - * @return string name of the panel - */ - public function getName() - { - return ''; - } + /** + * @return string name of the panel + */ + public function getName() + { + return ''; + } - /** - * @return string content that is displayed at debug toolbar - */ - public function getSummary() - { - return ''; - } + /** + * @return string content that is displayed at debug toolbar + */ + public function getSummary() + { + return ''; + } - /** - * @return string content that is displayed in debugger detail view - */ - public function getDetail() - { - return ''; - } + /** + * @return string content that is displayed in debugger detail view + */ + public function getDetail() + { + return ''; + } - /** - * Saves data to be later used in debugger detail view. - * This method is called on every page where debugger is enabled. - * - * @return mixed data to be saved - */ - public function save() - { - return null; - } + /** + * Saves data to be later used in debugger detail view. + * This method is called on every page where debugger is enabled. + * + * @return mixed data to be saved + */ + public function save() + { + return null; + } - /** - * Loads data into the panel - * - * @param mixed $data - */ - public function load($data) - { - $this->data = $data; - } + /** + * Loads data into the panel + * + * @param mixed $data + */ + public function load($data) + { + $this->data = $data; + } - /** - * @return string URL pointing to panel detail view - */ - public function getUrl() - { - return Yii::$app->getUrlManager()->createUrl([$this->module->id . '/default/view', - 'panel' => $this->id, - 'tag' => $this->tag, - ]); - } + /** + * @return string URL pointing to panel detail view + */ + public function getUrl() + { + return Yii::$app->getUrlManager()->createUrl([$this->module->id . '/default/view', + 'panel' => $this->id, + 'tag' => $this->tag, + ]); + } } diff --git a/extensions/debug/components/search/Filter.php b/extensions/debug/components/search/Filter.php index 55e6ebb3acf..09830824c18 100644 --- a/extensions/debug/components/search/Filter.php +++ b/extensions/debug/components/search/Filter.php @@ -18,63 +18,63 @@ */ class Filter extends Component { - /** - * @var array rules for matching filters in the way: [:fieldName => [rule1, rule2,..]] - */ - protected $rules = []; + /** + * @var array rules for matching filters in the way: [:fieldName => [rule1, rule2,..]] + */ + protected $rules = []; - /** - * Adds data filtering rule. - * - * @param string $name attribute name - * @param MatcherInterface $rule - */ - public function addMatcher($name, MatcherInterface $rule) - { - if ($rule->hasValue()) { - $this->rules[$name][] = $rule; - } - } + /** + * Adds data filtering rule. + * + * @param string $name attribute name + * @param MatcherInterface $rule + */ + public function addMatcher($name, MatcherInterface $rule) + { + if ($rule->hasValue()) { + $this->rules[$name][] = $rule; + } + } - /** - * Applies filter on a given array and returns filtered data. - * - * @param array $data data to filter - * @return array filtered data - */ - public function filter(array $data) - { - $filtered = []; + /** + * Applies filter on a given array and returns filtered data. + * + * @param array $data data to filter + * @return array filtered data + */ + public function filter(array $data) + { + $filtered = []; - foreach ($data as $row) { - if ($this->passesFilter($row)) { - $filtered[] = $row; - } - } + foreach ($data as $row) { + if ($this->passesFilter($row)) { + $filtered[] = $row; + } + } - return $filtered; - } + return $filtered; + } - /** - * Checks if the given data satisfies filters. - * - * @param array $row data - * @return boolean if data passed filtering - */ - private function passesFilter(array $row) - { - foreach ($row as $name => $value) { - if (isset($this->rules[$name])) { - // check all rules for a given attribute - foreach ($this->rules[$name] as $rule) { - /** @var MatcherInterface $rule */ - if (!$rule->match($value)) { - return false; - } - } - } - } + /** + * Checks if the given data satisfies filters. + * + * @param array $row data + * @return boolean if data passed filtering + */ + private function passesFilter(array $row) + { + foreach ($row as $name => $value) { + if (isset($this->rules[$name])) { + // check all rules for a given attribute + foreach ($this->rules[$name] as $rule) { + /** @var MatcherInterface $rule */ + if (!$rule->match($value)) { + return false; + } + } + } + } - return true; - } + return true; + } } diff --git a/extensions/debug/components/search/matchers/Base.php b/extensions/debug/components/search/matchers/Base.php index a29b40b2862..e98a7242243 100644 --- a/extensions/debug/components/search/matchers/Base.php +++ b/extensions/debug/components/search/matchers/Base.php @@ -17,24 +17,24 @@ */ abstract class Base extends Component implements MatcherInterface { - /** - * @var mixed base value to check - */ - protected $baseValue; + /** + * @var mixed base value to check + */ + protected $baseValue; - /** - * @inheritdoc - */ - public function setValue($value) - { - $this->baseValue = $value; - } + /** + * @inheritdoc + */ + public function setValue($value) + { + $this->baseValue = $value; + } - /** - * @inheritdoc - */ - public function hasValue() - { - return !empty($this->baseValue) || ($this->baseValue === '0'); - } + /** + * @inheritdoc + */ + public function hasValue() + { + return !empty($this->baseValue) || ($this->baseValue === '0'); + } } diff --git a/extensions/debug/components/search/matchers/GreaterThan.php b/extensions/debug/components/search/matchers/GreaterThan.php index 486fac462f2..abc907baabf 100644 --- a/extensions/debug/components/search/matchers/GreaterThan.php +++ b/extensions/debug/components/search/matchers/GreaterThan.php @@ -15,11 +15,11 @@ */ class GreaterThan extends Base { - /** - * @inheritdoc - */ - public function match($value) - { - return ($value > $this->baseValue); - } + /** + * @inheritdoc + */ + public function match($value) + { + return ($value > $this->baseValue); + } } diff --git a/extensions/debug/components/search/matchers/LowerThan.php b/extensions/debug/components/search/matchers/LowerThan.php index 018001a0a2c..c3d4f32b8e8 100644 --- a/extensions/debug/components/search/matchers/LowerThan.php +++ b/extensions/debug/components/search/matchers/LowerThan.php @@ -15,11 +15,11 @@ */ class LowerThan extends Base { - /** - * @inheritdoc - */ - public function match($value) - { - return ($value < $this->baseValue); - } + /** + * @inheritdoc + */ + public function match($value) + { + return ($value < $this->baseValue); + } } diff --git a/extensions/debug/components/search/matchers/MatcherInterface.php b/extensions/debug/components/search/matchers/MatcherInterface.php index febd06d68dd..c601bbc0029 100644 --- a/extensions/debug/components/search/matchers/MatcherInterface.php +++ b/extensions/debug/components/search/matchers/MatcherInterface.php @@ -9,31 +9,31 @@ /** * MatcherInterface should be implemented by all matchers that are used in a filter. - * + * * @author Mark Jebri * @since 2.0 */ interface MatcherInterface { - /** - * Checks if the value passed matches base value. - * - * @param mixed $value value to be matched - * @return boolean if there is a match - */ - public function match($value); + /** + * Checks if the value passed matches base value. + * + * @param mixed $value value to be matched + * @return boolean if there is a match + */ + public function match($value); - /** - * Sets base value to match against - * - * @param mixed $value - */ - public function setValue($value); + /** + * Sets base value to match against + * + * @param mixed $value + */ + public function setValue($value); - /** - * Checks if base value is set - * - * @return boolean if base value is set - */ - public function hasValue(); + /** + * Checks if base value is set + * + * @return boolean if base value is set + */ + public function hasValue(); } diff --git a/extensions/debug/components/search/matchers/SameAs.php b/extensions/debug/components/search/matchers/SameAs.php index ba3eedd7101..bb3088d187c 100644 --- a/extensions/debug/components/search/matchers/SameAs.php +++ b/extensions/debug/components/search/matchers/SameAs.php @@ -15,20 +15,20 @@ */ class SameAs extends Base { - /** - * @var boolean if partial match should be used. - */ - public $partial = false; + /** + * @var boolean if partial match should be used. + */ + public $partial = false; - /** - * @inheritdoc - */ - public function match($value) - { - if (!$this->partial) { - return (mb_strtolower($this->baseValue, 'utf8') == mb_strtolower($value, 'utf8')); - } else { - return (mb_strpos(mb_strtolower($value, 'utf8'), mb_strtolower($this->baseValue, 'utf8')) !== false); - } - } + /** + * @inheritdoc + */ + public function match($value) + { + if (!$this->partial) { + return (mb_strtolower($this->baseValue, 'utf8') == mb_strtolower($value, 'utf8')); + } else { + return (mb_strpos(mb_strtolower($value, 'utf8'), mb_strtolower($this->baseValue, 'utf8')) !== false); + } + } } diff --git a/extensions/debug/controllers/DefaultController.php b/extensions/debug/controllers/DefaultController.php index 9f64d40bc6c..ca227696770 100644 --- a/extensions/debug/controllers/DefaultController.php +++ b/extensions/debug/controllers/DefaultController.php @@ -20,133 +20,138 @@ */ class DefaultController extends Controller { - /** - * @inheritdoc - */ - public $layout = 'main'; - /** - * @var \yii\debug\Module - */ - public $module; - /** - * @var array the summary data (e.g. URL, time) - */ - public $summary; - - /** - * @inheritdoc - */ - public function actions() - { - $actions = []; - foreach ($this->module->panels as $panel) { - $actions = array_merge($actions, $panel->actions); - } - return $actions; - } - - public function actionIndex() - { - $searchModel = new Debug(); - $dataProvider = $searchModel->search($_GET, $this->getManifest()); - - // load latest request - $tags = array_keys($this->getManifest()); - $tag = reset($tags); - $this->loadData($tag); - - return $this->render('index', [ - 'panels' => $this->module->panels, - 'dataProvider' => $dataProvider, - 'searchModel' => $searchModel, - ]); - } - - public function actionView($tag = null, $panel = null) - { - if ($tag === null) { - $tags = array_keys($this->getManifest()); - $tag = reset($tags); - } - $this->loadData($tag); - if (isset($this->module->panels[$panel])) { - $activePanel = $this->module->panels[$panel]; - } else { - $activePanel = $this->module->panels['request']; - } - return $this->render('view', [ - 'tag' => $tag, - 'summary' => $this->summary, - 'manifest' => $this->getManifest(), - 'panels' => $this->module->panels, - 'activePanel' => $activePanel, - ]); - } - - public function actionToolbar($tag) - { - $this->loadData($tag, 5); - return $this->renderPartial('toolbar', [ - 'tag' => $tag, - 'panels' => $this->module->panels, - 'position' => 'bottom', - ]); - } - - public function actionDownloadMail($file) - { - $filePath = Yii::getAlias($this->module->panels['mail']->mailPath) . '/' . basename($file); - - if ((mb_strpos($file, '\\') !== false || mb_strpos($file, '/') !== false) || !is_file($filePath)) { - throw new NotFoundHttpException('Mail file not found'); - } - - Yii::$app->response->sendFile($filePath); - } - - private $_manifest; - - protected function getManifest($forceReload = false) - { - if ($this->_manifest === null || $forceReload) { - if ($forceReload) { - clearstatcache(); - } - $indexFile = $this->module->dataPath . '/index.data'; - if (is_file($indexFile)) { - $this->_manifest = array_reverse(unserialize(file_get_contents($indexFile)), true); - } else { - $this->_manifest = []; - } - } - return $this->_manifest; - } - - public function loadData($tag, $maxRetry = 0) - { - // retry loading debug data because the debug data is logged in shutdown function - // which may be delayed in some environment if xdebug is enabled. - // See: https://github.com/yiisoft/yii2/issues/1504 - for ($retry = 0; $retry <= $maxRetry; ++$retry) { - $manifest = $this->getManifest($retry > 0); - if (isset($manifest[$tag])) { - $dataFile = $this->module->dataPath . "/$tag.data"; - $data = unserialize(file_get_contents($dataFile)); - foreach ($this->module->panels as $id => $panel) { - if (isset($data[$id])) { - $panel->tag = $tag; - $panel->load($data[$id]); - } else { - // remove the panel since it has not received any data - unset($this->module->panels[$id]); - } - } - $this->summary = $data['summary']; - return; - } - sleep(1); - } - - throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'."); - } + /** + * @inheritdoc + */ + public $layout = 'main'; + /** + * @var \yii\debug\Module + */ + public $module; + /** + * @var array the summary data (e.g. URL, time) + */ + public $summary; + + /** + * @inheritdoc + */ + public function actions() + { + $actions = []; + foreach ($this->module->panels as $panel) { + $actions = array_merge($actions, $panel->actions); + } + + return $actions; + } + + public function actionIndex() + { + $searchModel = new Debug(); + $dataProvider = $searchModel->search($_GET, $this->getManifest()); + + // load latest request + $tags = array_keys($this->getManifest()); + $tag = reset($tags); + $this->loadData($tag); + + return $this->render('index', [ + 'panels' => $this->module->panels, + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel, + ]); + } + + public function actionView($tag = null, $panel = null) + { + if ($tag === null) { + $tags = array_keys($this->getManifest()); + $tag = reset($tags); + } + $this->loadData($tag); + if (isset($this->module->panels[$panel])) { + $activePanel = $this->module->panels[$panel]; + } else { + $activePanel = $this->module->panels['request']; + } + + return $this->render('view', [ + 'tag' => $tag, + 'summary' => $this->summary, + 'manifest' => $this->getManifest(), + 'panels' => $this->module->panels, + 'activePanel' => $activePanel, + ]); + } + + public function actionToolbar($tag) + { + $this->loadData($tag, 5); + + return $this->renderPartial('toolbar', [ + 'tag' => $tag, + 'panels' => $this->module->panels, + 'position' => 'bottom', + ]); + } + + public function actionDownloadMail($file) + { + $filePath = Yii::getAlias($this->module->panels['mail']->mailPath) . '/' . basename($file); + + if ((mb_strpos($file, '\\') !== false || mb_strpos($file, '/') !== false) || !is_file($filePath)) { + throw new NotFoundHttpException('Mail file not found'); + } + + Yii::$app->response->sendFile($filePath); + } + + private $_manifest; + + protected function getManifest($forceReload = false) + { + if ($this->_manifest === null || $forceReload) { + if ($forceReload) { + clearstatcache(); + } + $indexFile = $this->module->dataPath . '/index.data'; + if (is_file($indexFile)) { + $this->_manifest = array_reverse(unserialize(file_get_contents($indexFile)), true); + } else { + $this->_manifest = []; + } + } + + return $this->_manifest; + } + + public function loadData($tag, $maxRetry = 0) + { + // retry loading debug data because the debug data is logged in shutdown function + // which may be delayed in some environment if xdebug is enabled. + // See: https://github.com/yiisoft/yii2/issues/1504 + for ($retry = 0; $retry <= $maxRetry; ++$retry) { + $manifest = $this->getManifest($retry > 0); + if (isset($manifest[$tag])) { + $dataFile = $this->module->dataPath . "/$tag.data"; + $data = unserialize(file_get_contents($dataFile)); + foreach ($this->module->panels as $id => $panel) { + if (isset($data[$id])) { + $panel->tag = $tag; + $panel->load($data[$id]); + } else { + // remove the panel since it has not received any data + unset($this->module->panels[$id]); + } + } + $this->summary = $data['summary']; + + return; + } + sleep(1); + } + + throw new NotFoundHttpException("Unable to find debug data tagged with '$tag'."); + } } diff --git a/extensions/debug/models/search/Base.php b/extensions/debug/models/search/Base.php index f81c058a095..e8783a77ae8 100644 --- a/extensions/debug/models/search/Base.php +++ b/extensions/debug/models/search/Base.php @@ -19,26 +19,26 @@ */ class Base extends Model { - /** - * Adds filtering condition for a given attribute - * - * @param Filter $filter filter instance - * @param string $attribute attribute to filter - * @param boolean $partial if partial match should be used - */ - public function addCondition(Filter $filter, $attribute, $partial = false) - { - $value = $this->$attribute; + /** + * Adds filtering condition for a given attribute + * + * @param Filter $filter filter instance + * @param string $attribute attribute to filter + * @param boolean $partial if partial match should be used + */ + public function addCondition(Filter $filter, $attribute, $partial = false) + { + $value = $this->$attribute; - if (mb_strpos($value, '>') !== false) { - $value = intval(str_replace('>', '', $value)); - $filter->addMatcher($attribute, new matchers\GreaterThan(['value' => $value])); + if (mb_strpos($value, '>') !== false) { + $value = intval(str_replace('>', '', $value)); + $filter->addMatcher($attribute, new matchers\GreaterThan(['value' => $value])); - } elseif (mb_strpos($value, '<') !== false) { - $value = intval(str_replace('<', '', $value)); - $filter->addMatcher($attribute, new matchers\LowerThan(['value' => $value])); - } else { - $filter->addMatcher($attribute, new matchers\SameAs(['value' => $value, 'partial' => $partial])); - } - } + } elseif (mb_strpos($value, '<') !== false) { + $value = intval(str_replace('<', '', $value)); + $filter->addMatcher($attribute, new matchers\LowerThan(['value' => $value])); + } else { + $filter->addMatcher($attribute, new matchers\SameAs(['value' => $value, 'partial' => $partial])); + } + } } diff --git a/extensions/debug/models/search/Db.php b/extensions/debug/models/search/Db.php index 90ee26e031b..03ae1002507 100644 --- a/extensions/debug/models/search/Db.php +++ b/extensions/debug/models/search/Db.php @@ -19,66 +19,66 @@ */ class Db extends Base { - /** - * @var string type of the input search value - */ - public $type; + /** + * @var string type of the input search value + */ + public $type; - /** - * @var integer query attribute input search value - */ - public $query; + /** + * @var integer query attribute input search value + */ + public $query; - /** - * @inheritdoc - */ - public function rules() - { - return [ - [['type', 'query'], 'safe'], - ]; - } + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['type', 'query'], 'safe'], + ]; + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'type' => 'Type', - 'query' => 'Query', - ]; - } + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'type' => 'Type', + 'query' => 'Query', + ]; + } - /** - * Returns data provider with filled models. Filter applied if needed. - * - * @param array $params an array of parameter values indexed by parameter names - * @param array $models data to return provider for - * @return \yii\data\ArrayDataProvider - */ - public function search($params, $models) - { - $dataProvider = new ArrayDataProvider([ - 'allModels' => $models, - 'pagination' => false, - 'sort' => [ - 'attributes' => ['duration', 'seq', 'type', 'query'], - 'defaultOrder' => [ - 'duration' => SORT_DESC, - ], - ], - ]); + /** + * Returns data provider with filled models. Filter applied if needed. + * + * @param array $params an array of parameter values indexed by parameter names + * @param array $models data to return provider for + * @return \yii\data\ArrayDataProvider + */ + public function search($params, $models) + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => $models, + 'pagination' => false, + 'sort' => [ + 'attributes' => ['duration', 'seq', 'type', 'query'], + 'defaultOrder' => [ + 'duration' => SORT_DESC, + ], + ], + ]); - if (!($this->load($params) && $this->validate())) { - return $dataProvider; - } + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } - $filter = new Filter(); - $this->addCondition($filter, 'type', true); - $this->addCondition($filter, 'query', true); - $dataProvider->allModels = $filter->filter($models); + $filter = new Filter(); + $this->addCondition($filter, 'type', true); + $this->addCondition($filter, 'query', true); + $dataProvider->allModels = $filter->filter($models); - return $dataProvider; - } + return $dataProvider; + } } diff --git a/extensions/debug/models/search/Debug.php b/extensions/debug/models/search/Debug.php index c2d85e6bdba..9e6a406d647 100644 --- a/extensions/debug/models/search/Debug.php +++ b/extensions/debug/models/search/Debug.php @@ -19,122 +19,122 @@ */ class Debug extends Base { - /** - * @var string tag attribute input search value - */ - public $tag; - - /** - * @var string ip attribute input search value - */ - public $ip; - - /** - * @var string method attribute input search value - */ - public $method; - - /** - * @var integer ajax attribute input search value - */ - public $ajax; - - /** - * @var string url attribute input search value - */ - public $url; - - /** - * @var string status code attribute input search value - */ - public $statusCode; - - /** - * @var integer sql count attribute input search value - */ - public $sqlCount; - - /** - * @var integer total mail count attribute input search value - */ - public $mailCount; - - /** - * @var array critical codes, used to determine grid row options. - */ - public $criticalCodes = [400, 404, 500]; - - /** - * @inheritdoc - */ - public function rules() - { - return [ - [['tag', 'ip', 'method', 'ajax', 'url', 'statusCode', 'sqlCount', 'mailCount'], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'tag' => 'Tag', - 'ip' => 'Ip', - 'method' => 'Method', - 'ajax' => 'Ajax', - 'url' => 'url', - 'statusCode' => 'Status code', - 'sqlCount' => 'Query Count', - 'mailCount' => 'Mail Count', - ]; - } - - /** - * Returns data provider with filled models. Filter applied if needed. - * @param array $params an array of parameter values indexed by parameter names - * @param array $models data to return provider for - * @return \yii\data\ArrayDataProvider - */ - public function search($params, $models) - { - $dataProvider = new ArrayDataProvider([ - 'allModels' => $models, - 'sort' => [ - 'attributes' => ['method', 'ip', 'tag', 'time', 'statusCode', 'sqlCount', 'mailCount'], - ], - 'pagination' => [ - 'pageSize' => 50, - ], - ]); - - if (!($this->load($params) && $this->validate())) { - return $dataProvider; - } - - $filter = new Filter(); - $this->addCondition($filter, 'tag', true); - $this->addCondition($filter, 'ip', true); - $this->addCondition($filter, 'method'); - $this->addCondition($filter, 'ajax'); - $this->addCondition($filter, 'url', true); - $this->addCondition($filter, 'statusCode'); - $this->addCondition($filter, 'sqlCount'); - $this->addCondition($filter, 'mailCount'); - $dataProvider->allModels = $filter->filter($models); - - return $dataProvider; - } - - /** - * Checks if code is critical. - * - * @param integer $code - * @return boolean - */ - public function isCodeCritical($code) - { - return in_array($code, $this->criticalCodes); - } + /** + * @var string tag attribute input search value + */ + public $tag; + + /** + * @var string ip attribute input search value + */ + public $ip; + + /** + * @var string method attribute input search value + */ + public $method; + + /** + * @var integer ajax attribute input search value + */ + public $ajax; + + /** + * @var string url attribute input search value + */ + public $url; + + /** + * @var string status code attribute input search value + */ + public $statusCode; + + /** + * @var integer sql count attribute input search value + */ + public $sqlCount; + + /** + * @var integer total mail count attribute input search value + */ + public $mailCount; + + /** + * @var array critical codes, used to determine grid row options. + */ + public $criticalCodes = [400, 404, 500]; + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['tag', 'ip', 'method', 'ajax', 'url', 'statusCode', 'sqlCount', 'mailCount'], 'safe'], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'tag' => 'Tag', + 'ip' => 'Ip', + 'method' => 'Method', + 'ajax' => 'Ajax', + 'url' => 'url', + 'statusCode' => 'Status code', + 'sqlCount' => 'Query Count', + 'mailCount' => 'Mail Count', + ]; + } + + /** + * Returns data provider with filled models. Filter applied if needed. + * @param array $params an array of parameter values indexed by parameter names + * @param array $models data to return provider for + * @return \yii\data\ArrayDataProvider + */ + public function search($params, $models) + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => $models, + 'sort' => [ + 'attributes' => ['method', 'ip', 'tag', 'time', 'statusCode', 'sqlCount', 'mailCount'], + ], + 'pagination' => [ + 'pageSize' => 50, + ], + ]); + + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } + + $filter = new Filter(); + $this->addCondition($filter, 'tag', true); + $this->addCondition($filter, 'ip', true); + $this->addCondition($filter, 'method'); + $this->addCondition($filter, 'ajax'); + $this->addCondition($filter, 'url', true); + $this->addCondition($filter, 'statusCode'); + $this->addCondition($filter, 'sqlCount'); + $this->addCondition($filter, 'mailCount'); + $dataProvider->allModels = $filter->filter($models); + + return $dataProvider; + } + + /** + * Checks if code is critical. + * + * @param integer $code + * @return boolean + */ + public function isCodeCritical($code) + { + return in_array($code, $this->criticalCodes); + } } diff --git a/extensions/debug/models/search/Log.php b/extensions/debug/models/search/Log.php index ba19c5aaae6..56b3057a52b 100644 --- a/extensions/debug/models/search/Log.php +++ b/extensions/debug/models/search/Log.php @@ -19,70 +19,70 @@ */ class Log extends Base { - /** - * @var string ip attribute input search value - */ - public $level; + /** + * @var string ip attribute input search value + */ + public $level; - /** - * @var string method attribute input search value - */ - public $category; + /** + * @var string method attribute input search value + */ + public $category; - /** - * @var integer message attribute input search value - */ - public $message; + /** + * @var integer message attribute input search value + */ + public $message; - /** - * @inheritdoc - */ - public function rules() - { - return [ - [['level', 'message', 'category'], 'safe'], - ]; - } + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['level', 'message', 'category'], 'safe'], + ]; + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'level' => 'Level', - 'category' => 'Category', - 'message' => 'Message', - ]; - } + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'level' => 'Level', + 'category' => 'Category', + 'message' => 'Message', + ]; + } - /** - * Returns data provider with filled models. Filter applied if needed. - * - * @param array $params an array of parameter values indexed by parameter names - * @param array $models data to return provider for - * @return \yii\data\ArrayDataProvider - */ - public function search($params, $models) - { - $dataProvider = new ArrayDataProvider([ - 'allModels' => $models, - 'pagination' => false, - 'sort' => [ - 'attributes' => ['time', 'level', 'category', 'message'], - ], - ]); + /** + * Returns data provider with filled models. Filter applied if needed. + * + * @param array $params an array of parameter values indexed by parameter names + * @param array $models data to return provider for + * @return \yii\data\ArrayDataProvider + */ + public function search($params, $models) + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => $models, + 'pagination' => false, + 'sort' => [ + 'attributes' => ['time', 'level', 'category', 'message'], + ], + ]); - if (!($this->load($params) && $this->validate())) { - return $dataProvider; - } + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } - $filter = new Filter(); - $this->addCondition($filter, 'level'); - $this->addCondition($filter, 'category', true); - $this->addCondition($filter, 'message', true); - $dataProvider->allModels = $filter->filter($models); + $filter = new Filter(); + $this->addCondition($filter, 'level'); + $this->addCondition($filter, 'category', true); + $this->addCondition($filter, 'message', true); + $dataProvider->allModels = $filter->filter($models); - return $dataProvider; - } + return $dataProvider; + } } diff --git a/extensions/debug/models/search/Mail.php b/extensions/debug/models/search/Mail.php index 3cd7a279cfa..5afeb98a282 100644 --- a/extensions/debug/models/search/Mail.php +++ b/extensions/debug/models/search/Mail.php @@ -13,112 +13,112 @@ */ class Mail extends Base { - /** - * @var string from attribute input search value - */ - public $from; - - /** - * @var string to attribute input search value - */ - public $to; - - /** - * @var string reply attribute input search value - */ - public $reply; - - /** - * @var string cc attribute input search value - */ - public $cc; - - /** - * @var string bcc attribute input search value - */ - public $bcc; - - /** - * @var string subject attribute input search value - */ - public $subject; - - /** - * @var string body attribute input search value - */ - public $body; - - /** - * @var string charset attribute input search value - */ - public $charset; - - /** - * @var string headers attribute input search value - */ - public $headers; - - /** - * @var string file attribute input search value - */ - public $file; - - public function rules() - { - return [ - [['from', 'to', 'reply', 'cc', 'bcc', 'subject', 'body', 'charset'], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'from' => 'From', - 'to' => 'To', - 'reply' => 'Reply', - 'cc' => 'Copy receiver', - 'bcc' => 'Hidden copy receiver', - 'subject' => 'Subject', - 'charset' => 'Charset' - ]; - } - - /** - * Returns data provider with filled models. Filter applied if needed. - * @param array $params - * @param array $models - * @return \yii\data\ArrayDataProvider - */ - public function search($params, $models) - { - $dataProvider = new ArrayDataProvider([ - 'allModels' => $models, - 'pagination' => [ - 'pageSize' => 20, - ], - 'sort' => [ - 'attributes' => ['from', 'to', 'reply', 'cc', 'bcc', 'subject', 'body', 'charset'], - ], - ]); - - if (!($this->load($params) && $this->validate())) { - return $dataProvider; - } - - $filter = new Filter(); - $this->addCondition($filter, 'from', true); - $this->addCondition($filter, 'to', true); - $this->addCondition($filter, 'reply', true); - $this->addCondition($filter, 'cc', true); - $this->addCondition($filter, 'bcc', true); - $this->addCondition($filter, 'subject', true); - $this->addCondition($filter, 'body', true); - $this->addCondition($filter, 'charset', true); - $dataProvider->allModels = $filter->filter($models); - - return $dataProvider; - } + /** + * @var string from attribute input search value + */ + public $from; + + /** + * @var string to attribute input search value + */ + public $to; + + /** + * @var string reply attribute input search value + */ + public $reply; + + /** + * @var string cc attribute input search value + */ + public $cc; + + /** + * @var string bcc attribute input search value + */ + public $bcc; + + /** + * @var string subject attribute input search value + */ + public $subject; + + /** + * @var string body attribute input search value + */ + public $body; + + /** + * @var string charset attribute input search value + */ + public $charset; + + /** + * @var string headers attribute input search value + */ + public $headers; + + /** + * @var string file attribute input search value + */ + public $file; + + public function rules() + { + return [ + [['from', 'to', 'reply', 'cc', 'bcc', 'subject', 'body', 'charset'], 'safe'], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'from' => 'From', + 'to' => 'To', + 'reply' => 'Reply', + 'cc' => 'Copy receiver', + 'bcc' => 'Hidden copy receiver', + 'subject' => 'Subject', + 'charset' => 'Charset' + ]; + } + + /** + * Returns data provider with filled models. Filter applied if needed. + * @param array $params + * @param array $models + * @return \yii\data\ArrayDataProvider + */ + public function search($params, $models) + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => $models, + 'pagination' => [ + 'pageSize' => 20, + ], + 'sort' => [ + 'attributes' => ['from', 'to', 'reply', 'cc', 'bcc', 'subject', 'body', 'charset'], + ], + ]); + + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } + + $filter = new Filter(); + $this->addCondition($filter, 'from', true); + $this->addCondition($filter, 'to', true); + $this->addCondition($filter, 'reply', true); + $this->addCondition($filter, 'cc', true); + $this->addCondition($filter, 'bcc', true); + $this->addCondition($filter, 'subject', true); + $this->addCondition($filter, 'body', true); + $this->addCondition($filter, 'charset', true); + $dataProvider->allModels = $filter->filter($models); + + return $dataProvider; + } } diff --git a/extensions/debug/models/search/Profile.php b/extensions/debug/models/search/Profile.php index f39f4ca98ec..79c31a63ddb 100644 --- a/extensions/debug/models/search/Profile.php +++ b/extensions/debug/models/search/Profile.php @@ -19,66 +19,66 @@ */ class Profile extends Base { - /** - * @var string method attribute input search value - */ - public $category; + /** + * @var string method attribute input search value + */ + public $category; - /** - * @var integer info attribute input search value - */ - public $info; + /** + * @var integer info attribute input search value + */ + public $info; - /** - * @inheritdoc - */ - public function rules() - { - return [ - [['category', 'info'], 'safe'], - ]; - } + /** + * @inheritdoc + */ + public function rules() + { + return [ + [['category', 'info'], 'safe'], + ]; + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'category' => 'Category', - 'info' => 'Info', - ]; - } + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'category' => 'Category', + 'info' => 'Info', + ]; + } - /** - * Returns data provider with filled models. Filter applied if needed. - * - * @param array $params an array of parameter values indexed by parameter names - * @param array $models data to return provider for - * @return \yii\data\ArrayDataProvider - */ - public function search($params, $models) - { - $dataProvider = new ArrayDataProvider([ - 'allModels' => $models, - 'pagination' => false, - 'sort' => [ - 'attributes' => ['category', 'seq', 'duration', 'info'], - 'defaultOrder' => [ - 'seq' => SORT_ASC, - ], - ], - ]); + /** + * Returns data provider with filled models. Filter applied if needed. + * + * @param array $params an array of parameter values indexed by parameter names + * @param array $models data to return provider for + * @return \yii\data\ArrayDataProvider + */ + public function search($params, $models) + { + $dataProvider = new ArrayDataProvider([ + 'allModels' => $models, + 'pagination' => false, + 'sort' => [ + 'attributes' => ['category', 'seq', 'duration', 'info'], + 'defaultOrder' => [ + 'seq' => SORT_ASC, + ], + ], + ]); - if (!($this->load($params) && $this->validate())) { - return $dataProvider; - } + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } - $filter = new Filter(); - $this->addCondition($filter, 'category', true); - $this->addCondition($filter, 'info', true); - $dataProvider->allModels = $filter->filter($models); + $filter = new Filter(); + $this->addCondition($filter, 'category', true); + $this->addCondition($filter, 'info', true); + $dataProvider->allModels = $filter->filter($models); - return $dataProvider; - } + return $dataProvider; + } } diff --git a/extensions/debug/panels/ConfigPanel.php b/extensions/debug/panels/ConfigPanel.php index 4ac76b44081..048aef89865 100644 --- a/extensions/debug/panels/ConfigPanel.php +++ b/extensions/debug/panels/ConfigPanel.php @@ -20,82 +20,83 @@ */ class ConfigPanel extends Panel { - /** - * @inheritdoc - */ - public function getName() - { - return 'Configuration'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'Configuration'; + } - /** - * @inheritdoc - */ - public function getSummary() - { - return Yii::$app->view->render('panels/config/summary', ['panel' => $this]); - } + /** + * @inheritdoc + */ + public function getSummary() + { + return Yii::$app->view->render('panels/config/summary', ['panel' => $this]); + } - /** - * @inheritdoc - */ - public function getDetail() - { - return Yii::$app->view->render('panels/config/detail', ['panel' => $this]); - } + /** + * @inheritdoc + */ + public function getDetail() + { + return Yii::$app->view->render('panels/config/detail', ['panel' => $this]); + } - /** - * Returns data about extensions - * - * @return array - */ - public function getExtensions() - { - $data = []; - foreach ($this->data['extensions'] as $extension) { - $data[$extension['name']] = $extension['version']; - } - return $data; - } + /** + * Returns data about extensions + * + * @return array + */ + public function getExtensions() + { + $data = []; + foreach ($this->data['extensions'] as $extension) { + $data[$extension['name']] = $extension['version']; + } - /** - * Returns the BODY contents of the phpinfo() output - * - * @return array - */ - public function getPhpInfo () - { - ob_start(); - phpinfo(); - $pinfo = ob_get_contents(); - ob_end_clean(); - $phpinfo = preg_replace('%^.*(.*).*$%ms', '$1', $pinfo); - $phpinfo = str_replace('(.*).*$%ms', '$1', $pinfo); + $phpinfo = str_replace('
PHP_VERSION, - 'yiiVersion' => Yii::getVersion(), - 'application' => [ - 'yii' => Yii::getVersion(), - 'name' => Yii::$app->name, - 'env' => YII_ENV, - 'debug' => YII_DEBUG, - ], - 'php' => [ - 'version' => PHP_VERSION, - 'xdebug' => extension_loaded('xdebug'), - 'apc' => extension_loaded('apc'), - 'memcache' => extension_loaded('memcache'), - ], - 'extensions' => Yii::$app->extensions, - ]; - } + return $phpinfo; + } + + /** + * @inheritdoc + */ + public function save() + { + return [ + 'phpVersion' => PHP_VERSION, + 'yiiVersion' => Yii::getVersion(), + 'application' => [ + 'yii' => Yii::getVersion(), + 'name' => Yii::$app->name, + 'env' => YII_ENV, + 'debug' => YII_DEBUG, + ], + 'php' => [ + 'version' => PHP_VERSION, + 'xdebug' => extension_loaded('xdebug'), + 'apc' => extension_loaded('apc'), + 'memcache' => extension_loaded('memcache'), + ], + 'extensions' => Yii::$app->extensions, + ]; + } } diff --git a/extensions/debug/panels/DbPanel.php b/extensions/debug/panels/DbPanel.php index c95cc510013..db20ae22cb8 100644 --- a/extensions/debug/panels/DbPanel.php +++ b/extensions/debug/panels/DbPanel.php @@ -22,157 +22,161 @@ */ class DbPanel extends Panel { - /** - * @var integer the threshold for determining whether the request has involved - * critical number of DB queries. If the number of queries exceeds this number, - * the execution is considered taking critical number of DB queries. - */ - public $criticalQueryThreshold; - /** - * @var array db queries info extracted to array as models, to use with data provider. - */ - private $_models; - - /** - * @var array current database request timings - */ - private $_timings; - - /** - * @inheritdoc - */ - public function getName() - { - return 'Database'; - } - - /** - * @inheritdoc - */ - public function getSummary() - { - $timings = $this->calculateTimings(); - $queryCount = count($timings); - $queryTime = number_format($this->getTotalQueryTime($timings) * 1000) . ' ms'; - - return Yii::$app->view->render('panels/db/summary', [ - 'timings' => $this->calculateTimings(), - 'panel' => $this, - 'queryCount' => $queryCount, - 'queryTime' => $queryTime, - ]); - } - - /** - * @inheritdoc - */ - public function getDetail() - { - $searchModel = new Db(); - $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); - - return Yii::$app->view->render('panels/db/detail', [ - 'panel' => $this, - 'dataProvider' => $dataProvider, - 'searchModel' => $searchModel, - ]); - } - - /** - * Calculates given request profile timings. - * - * @return array timings [token, category, timestamp, traces, nesting level, elapsed time] - */ - protected function calculateTimings() - { - if ($this->_timings === null) { - $this->_timings = Yii::$app->getLog()->calculateTimings($this->data['messages']); - } - return $this->_timings; - } - - /** - * @inheritdoc - */ - public function save() - { - return ['messages' => $this->getProfileLogs()]; - } - - /** - * Returns all profile logs of the current request for this panel. It includes categories such as: - * 'yii\db\Command::query', 'yii\db\Command::execute'. - * @return array - */ - public function getProfileLogs() - { - $target = $this->module->logTarget; - return $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\db\Command::query', 'yii\db\Command::execute']); - } - - /** - * Returns total query time. - * - * @param array $timings - * @return integer total time - */ - protected function getTotalQueryTime($timings) - { - $queryTime = 0; - - foreach ($timings as $timing) { - $queryTime += $timing['duration']; - } - - return $queryTime; - } - - /** - * Returns an array of models that represents logs of the current request. - * Can be used with data providers such as \yii\data\ArrayDataProvider. - * @return array models - */ - protected function getModels() - { - if ($this->_models === null) { - $this->_models = []; - $timings = $this->calculateTimings(); - - foreach ($timings as $seq => $dbTiming) { - $this->_models[] = [ - 'type' => $this->getQueryType($dbTiming['info']), - 'query' => $dbTiming['info'], - 'duration' => ($dbTiming['duration'] * 1000), // in milliseconds - 'trace' => $dbTiming['trace'], - 'timestamp' => ($dbTiming['timestamp'] * 1000), // in milliseconds - 'seq' => $seq, - ]; - } - } - return $this->_models; - } - - /** - * Returns databse query type. - * - * @param string $timing timing procedure string - * @return string query type such as select, insert, delete, etc. - */ - protected function getQueryType($timing) - { - $timing = ltrim($timing); - preg_match('/^([a-zA-z]*)/', $timing, $matches); - return count($matches) ? $matches[0] : ''; - } - - /** - * Check if given queries count is critical according settings. - * - * @param integer $count queries count - * @return boolean - */ - public function isQueryCountCritical($count) - { - return (($this->criticalQueryThreshold !== null) && ($count > $this->criticalQueryThreshold)); - } + /** + * @var integer the threshold for determining whether the request has involved + * critical number of DB queries. If the number of queries exceeds this number, + * the execution is considered taking critical number of DB queries. + */ + public $criticalQueryThreshold; + /** + * @var array db queries info extracted to array as models, to use with data provider. + */ + private $_models; + + /** + * @var array current database request timings + */ + private $_timings; + + /** + * @inheritdoc + */ + public function getName() + { + return 'Database'; + } + + /** + * @inheritdoc + */ + public function getSummary() + { + $timings = $this->calculateTimings(); + $queryCount = count($timings); + $queryTime = number_format($this->getTotalQueryTime($timings) * 1000) . ' ms'; + + return Yii::$app->view->render('panels/db/summary', [ + 'timings' => $this->calculateTimings(), + 'panel' => $this, + 'queryCount' => $queryCount, + 'queryTime' => $queryTime, + ]); + } + + /** + * @inheritdoc + */ + public function getDetail() + { + $searchModel = new Db(); + $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); + + return Yii::$app->view->render('panels/db/detail', [ + 'panel' => $this, + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel, + ]); + } + + /** + * Calculates given request profile timings. + * + * @return array timings [token, category, timestamp, traces, nesting level, elapsed time] + */ + protected function calculateTimings() + { + if ($this->_timings === null) { + $this->_timings = Yii::$app->getLog()->calculateTimings($this->data['messages']); + } + + return $this->_timings; + } + + /** + * @inheritdoc + */ + public function save() + { + return ['messages' => $this->getProfileLogs()]; + } + + /** + * Returns all profile logs of the current request for this panel. It includes categories such as: + * 'yii\db\Command::query', 'yii\db\Command::execute'. + * @return array + */ + public function getProfileLogs() + { + $target = $this->module->logTarget; + + return $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\db\Command::query', 'yii\db\Command::execute']); + } + + /** + * Returns total query time. + * + * @param array $timings + * @return integer total time + */ + protected function getTotalQueryTime($timings) + { + $queryTime = 0; + + foreach ($timings as $timing) { + $queryTime += $timing['duration']; + } + + return $queryTime; + } + + /** + * Returns an array of models that represents logs of the current request. + * Can be used with data providers such as \yii\data\ArrayDataProvider. + * @return array models + */ + protected function getModels() + { + if ($this->_models === null) { + $this->_models = []; + $timings = $this->calculateTimings(); + + foreach ($timings as $seq => $dbTiming) { + $this->_models[] = [ + 'type' => $this->getQueryType($dbTiming['info']), + 'query' => $dbTiming['info'], + 'duration' => ($dbTiming['duration'] * 1000), // in milliseconds + 'trace' => $dbTiming['trace'], + 'timestamp' => ($dbTiming['timestamp'] * 1000), // in milliseconds + 'seq' => $seq, + ]; + } + } + + return $this->_models; + } + + /** + * Returns databse query type. + * + * @param string $timing timing procedure string + * @return string query type such as select, insert, delete, etc. + */ + protected function getQueryType($timing) + { + $timing = ltrim($timing); + preg_match('/^([a-zA-z]*)/', $timing, $matches); + + return count($matches) ? $matches[0] : ''; + } + + /** + * Check if given queries count is critical according settings. + * + * @param integer $count queries count + * @return boolean + */ + public function isQueryCountCritical($count) + { + return (($this->criticalQueryThreshold !== null) && ($count > $this->criticalQueryThreshold)); + } } diff --git a/extensions/debug/panels/LogPanel.php b/extensions/debug/panels/LogPanel.php index 67b9773734f..e5f3482a93f 100644 --- a/extensions/debug/panels/LogPanel.php +++ b/extensions/debug/panels/LogPanel.php @@ -20,74 +20,76 @@ */ class LogPanel extends Panel { - /** - * @var array log messages extracted to array as models, to use with data provider. - */ - private $_models; + /** + * @var array log messages extracted to array as models, to use with data provider. + */ + private $_models; - /** - * @inheritdoc - */ - public function getName() - { - return 'Logs'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'Logs'; + } - /** - * @inheritdoc - */ - public function getSummary() - { - return Yii::$app->view->render('panels/log/summary', ['data' => $this->data, 'panel' => $this]); - } + /** + * @inheritdoc + */ + public function getSummary() + { + return Yii::$app->view->render('panels/log/summary', ['data' => $this->data, 'panel' => $this]); + } - /** - * @inheritdoc - */ - public function getDetail() - { - $searchModel = new Log(); - $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); + /** + * @inheritdoc + */ + public function getDetail() + { + $searchModel = new Log(); + $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); - return Yii::$app->view->render('panels/log/detail', [ - 'dataProvider' => $dataProvider, - 'panel' => $this, - 'searchModel' => $searchModel, - ]); - } + return Yii::$app->view->render('panels/log/detail', [ + 'dataProvider' => $dataProvider, + 'panel' => $this, + 'searchModel' => $searchModel, + ]); + } - /** - * @inheritdoc - */ - public function save() - { - $target = $this->module->logTarget; - $messages = $target->filterMessages($target->messages, Logger::LEVEL_ERROR | Logger::LEVEL_INFO | Logger::LEVEL_WARNING | Logger::LEVEL_TRACE); - return ['messages' => $messages]; - } + /** + * @inheritdoc + */ + public function save() + { + $target = $this->module->logTarget; + $messages = $target->filterMessages($target->messages, Logger::LEVEL_ERROR | Logger::LEVEL_INFO | Logger::LEVEL_WARNING | Logger::LEVEL_TRACE); - /** - * Returns an array of models that represents logs of the current request. - * Can be used with data providers, such as \yii\data\ArrayDataProvider. - * - * @param boolean $refresh if need to build models from log messages and refresh them. - * @return array models - */ - protected function getModels($refresh = false) - { - if ($this->_models === null || $refresh) { - $this->_models = []; + return ['messages' => $messages]; + } - foreach ($this->data['messages'] as $message) { - $this->_models[] = [ - 'message' => $message[0], - 'level' => $message[1], - 'category' => $message[2], - 'time' => ($message[3] * 1000), // time in milliseconds - 'trace' => $message[4] - ]; - } - } - return $this->_models; - } + /** + * Returns an array of models that represents logs of the current request. + * Can be used with data providers, such as \yii\data\ArrayDataProvider. + * + * @param boolean $refresh if need to build models from log messages and refresh them. + * @return array models + */ + protected function getModels($refresh = false) + { + if ($this->_models === null || $refresh) { + $this->_models = []; + + foreach ($this->data['messages'] as $message) { + $this->_models[] = [ + 'message' => $message[0], + 'level' => $message[1], + 'category' => $message[2], + 'time' => ($message[3] * 1000), // time in milliseconds + 'trace' => $message[4] + ]; + } + } + + return $this->_models; + } } diff --git a/extensions/debug/panels/MailPanel.php b/extensions/debug/panels/MailPanel.php index 5193db284ec..a074b9950fa 100644 --- a/extensions/debug/panels/MailPanel.php +++ b/extensions/debug/panels/MailPanel.php @@ -20,86 +20,87 @@ class MailPanel extends Panel { - /** - * @var string path where all emails will be saved. should be an alias. - */ - public $mailPath = '@runtime/debug/mail'; - /** - * @var array current request sent messages - */ - private $_messages = []; - - public function init() - { - parent::init(); - Event::on(BaseMailer::className(), BaseMailer::EVENT_AFTER_SEND, function ($event) { - - $message = $event->message->getSwiftMessage(); - $textBody = $message->getBody(); - $fileName = $event->sender->generateMessageFileName(); - - FileHelper::createDirectory(Yii::getAlias($this->mailPath)); - file_put_contents(Yii::getAlias($this->mailPath) . '/' . $fileName, $message->toString()); - - $this->_messages[] = [ - 'isSuccessful' => $event->isSuccessful, - 'time' => $message->getDate(), - 'headers' => $message->getHeaders(), - 'from' => $this->convertParams($message->getFrom()), - 'to' => $this->convertParams($message->getTo()), - 'reply' => $this->convertParams($message->getReplyTo()), - 'cc' => $this->convertParams($message->getCc()), - 'bcc' => $this->convertParams($message->getBcc()), - 'subject' => $message->getSubject(), - 'body' => $textBody, - 'charset' => $message->getCharset(), - 'file' => $fileName, - ]; - }); - } - - public function getName() - { - return 'Mail'; - } - - public function getSummary() - { - return Yii::$app->view->render('panels/mail/summary', ['panel' => $this, 'mailCount' => count($this->data)]); - } - - public function getDetail() - { - $searchModel = new Mail(); - $dataProvider = $searchModel->search(Yii::$app->request->get(), $this->data); - - return Yii::$app->view->render('panels/mail/detail', [ - 'panel' => $this, - 'dataProvider' => $dataProvider, - 'searchModel' => $searchModel - ]); - } - - public function save() - { - return $this->getMessages(); - } - - /** - * Returns info about messages of current request. Each element is array holding - * message info, such as: time, reply, bc, cc, from, to and other. - * @return array messages - */ - public function getMessages() - { - return $this->_messages; - } - - private function convertParams($attr) - { - if (is_array($attr)) { - $attr = implode(', ', array_keys($attr)); - } - return $attr; - } + /** + * @var string path where all emails will be saved. should be an alias. + */ + public $mailPath = '@runtime/debug/mail'; + /** + * @var array current request sent messages + */ + private $_messages = []; + + public function init() + { + parent::init(); + Event::on(BaseMailer::className(), BaseMailer::EVENT_AFTER_SEND, function ($event) { + + $message = $event->message->getSwiftMessage(); + $textBody = $message->getBody(); + $fileName = $event->sender->generateMessageFileName(); + + FileHelper::createDirectory(Yii::getAlias($this->mailPath)); + file_put_contents(Yii::getAlias($this->mailPath) . '/' . $fileName, $message->toString()); + + $this->_messages[] = [ + 'isSuccessful' => $event->isSuccessful, + 'time' => $message->getDate(), + 'headers' => $message->getHeaders(), + 'from' => $this->convertParams($message->getFrom()), + 'to' => $this->convertParams($message->getTo()), + 'reply' => $this->convertParams($message->getReplyTo()), + 'cc' => $this->convertParams($message->getCc()), + 'bcc' => $this->convertParams($message->getBcc()), + 'subject' => $message->getSubject(), + 'body' => $textBody, + 'charset' => $message->getCharset(), + 'file' => $fileName, + ]; + }); + } + + public function getName() + { + return 'Mail'; + } + + public function getSummary() + { + return Yii::$app->view->render('panels/mail/summary', ['panel' => $this, 'mailCount' => count($this->data)]); + } + + public function getDetail() + { + $searchModel = new Mail(); + $dataProvider = $searchModel->search(Yii::$app->request->get(), $this->data); + + return Yii::$app->view->render('panels/mail/detail', [ + 'panel' => $this, + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel + ]); + } + + public function save() + { + return $this->getMessages(); + } + + /** + * Returns info about messages of current request. Each element is array holding + * message info, such as: time, reply, bc, cc, from, to and other. + * @return array messages + */ + public function getMessages() + { + return $this->_messages; + } + + private function convertParams($attr) + { + if (is_array($attr)) { + $attr = implode(', ', array_keys($attr)); + } + + return $attr; + } } diff --git a/extensions/debug/panels/ProfilingPanel.php b/extensions/debug/panels/ProfilingPanel.php index b287bfe2e58..33f698413d6 100644 --- a/extensions/debug/panels/ProfilingPanel.php +++ b/extensions/debug/panels/ProfilingPanel.php @@ -20,83 +20,85 @@ */ class ProfilingPanel extends Panel { - /** - * @var array current request profile timings - */ - private $_models; + /** + * @var array current request profile timings + */ + private $_models; - /** - * @inheritdoc - */ - public function getName() - { - return 'Profiling'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'Profiling'; + } - /** - * @inheritdoc - */ - public function getSummary() - { - return Yii::$app->view->render('panels/profile/summary', [ - 'memory' => sprintf('%.1f MB', $this->data['memory'] / 1048576), - 'time' => number_format($this->data['time'] * 1000) . ' ms', - 'panel' => $this - ]); - } + /** + * @inheritdoc + */ + public function getSummary() + { + return Yii::$app->view->render('panels/profile/summary', [ + 'memory' => sprintf('%.1f MB', $this->data['memory'] / 1048576), + 'time' => number_format($this->data['time'] * 1000) . ' ms', + 'panel' => $this + ]); + } - /** - * @inheritdoc - */ - public function getDetail() - { - $searchModel = new Profile(); - $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); + /** + * @inheritdoc + */ + public function getDetail() + { + $searchModel = new Profile(); + $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); - return Yii::$app->view->render('panels/profile/detail', [ - 'panel' => $this, - 'dataProvider' => $dataProvider, - 'searchModel' => $searchModel, - 'memory' => sprintf('%.1f MB', $this->data['memory'] / 1048576), - 'time' => number_format($this->data['time'] * 1000) . ' ms', - ]); - } + return Yii::$app->view->render('panels/profile/detail', [ + 'panel' => $this, + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel, + 'memory' => sprintf('%.1f MB', $this->data['memory'] / 1048576), + 'time' => number_format($this->data['time'] * 1000) . ' ms', + ]); + } - /** - * @inheritdoc - */ - public function save() - { - $target = $this->module->logTarget; - $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE); - return [ - 'memory' => memory_get_peak_usage(), - 'time' => microtime(true) - YII_BEGIN_TIME, - 'messages' => $messages, - ]; - } + /** + * @inheritdoc + */ + public function save() + { + $target = $this->module->logTarget; + $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE); - /** - * Returns array of profiling models that can be used in a data provider. - * @return array models - */ - protected function getModels() - { - if ($this->_models === null) { - $this->_models = []; - $timings = Yii::$app->getLog()->calculateTimings($this->data['messages']); + return [ + 'memory' => memory_get_peak_usage(), + 'time' => microtime(true) - YII_BEGIN_TIME, + 'messages' => $messages, + ]; + } - foreach ($timings as $seq => $profileTiming) { - $this->_models[] = [ - 'duration' => $profileTiming['duration'] * 1000, // in milliseconds - 'category' => $profileTiming['category'], - 'info' => $profileTiming['info'], - 'level' => $profileTiming['level'], - 'timestamp' => $profileTiming['timestamp'] * 1000, //in milliseconds - 'seq' => $seq, - ]; - } - } - return $this->_models; - } + /** + * Returns array of profiling models that can be used in a data provider. + * @return array models + */ + protected function getModels() + { + if ($this->_models === null) { + $this->_models = []; + $timings = Yii::$app->getLog()->calculateTimings($this->data['messages']); + + foreach ($timings as $seq => $profileTiming) { + $this->_models[] = [ + 'duration' => $profileTiming['duration'] * 1000, // in milliseconds + 'category' => $profileTiming['category'], + 'info' => $profileTiming['info'], + 'level' => $profileTiming['level'], + 'timestamp' => $profileTiming['timestamp'] * 1000, //in milliseconds + 'seq' => $seq, + ]; + } + } + + return $this->_models; + } } diff --git a/extensions/debug/panels/RequestPanel.php b/extensions/debug/panels/RequestPanel.php index f2042573f0b..53971c1688a 100644 --- a/extensions/debug/panels/RequestPanel.php +++ b/extensions/debug/panels/RequestPanel.php @@ -19,93 +19,94 @@ */ class RequestPanel extends Panel { - /** - * @inheritdoc - */ - public function getName() - { - return 'Request'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'Request'; + } - /** - * @inheritdoc - */ - public function getSummary() - { - return Yii::$app->view->render('panels/request/summary', ['panel' => $this]); - } + /** + * @inheritdoc + */ + public function getSummary() + { + return Yii::$app->view->render('panels/request/summary', ['panel' => $this]); + } - /** - * @inheritdoc - */ - public function getDetail() - { - return Yii::$app->view->render('panels/request/detail', ['panel' => $this]); - } + /** + * @inheritdoc + */ + public function getDetail() + { + return Yii::$app->view->render('panels/request/detail', ['panel' => $this]); + } - /** - * @inheritdoc - */ - public function save() - { - $headers = Yii::$app->getRequest()->getHeaders(); - $requestHeaders = []; - foreach ($headers as $name => $value) { - if (is_array($value) && count($value) == 1) { - $requestHeaders[$name] = current($value); - } else { - $requestHeaders[$name] = $value; - } - } + /** + * @inheritdoc + */ + public function save() + { + $headers = Yii::$app->getRequest()->getHeaders(); + $requestHeaders = []; + foreach ($headers as $name => $value) { + if (is_array($value) && count($value) == 1) { + $requestHeaders[$name] = current($value); + } else { + $requestHeaders[$name] = $value; + } + } - $responseHeaders = []; - foreach (headers_list() as $header) { - if (($pos = strpos($header, ':')) !== false) { - $name = substr($header, 0, $pos); - $value = trim(substr($header, $pos + 1)); - if (isset($responseHeaders[$name])) { - if (!is_array($responseHeaders[$name])) { - $responseHeaders[$name] = [$responseHeaders[$name], $value]; - } else { - $responseHeaders[$name][] = $value; - } - } else { - $responseHeaders[$name] = $value; - } - } else { - $responseHeaders[] = $header; - } - } - if (Yii::$app->requestedAction) { - if (Yii::$app->requestedAction instanceof InlineAction) { - $action = get_class(Yii::$app->requestedAction->controller) . '::' . Yii::$app->requestedAction->actionMethod . '()'; - } else { - $action = get_class(Yii::$app->requestedAction) . '::run()'; - } - } else { - $action = null; - } - /** @var \yii\web\Session $session */ - $session = Yii::$app->getComponent('session', false); - return [ - 'flashes' => $session ? $session->getAllFlashes() : [], - 'statusCode' => Yii::$app->getResponse()->getStatusCode(), - 'requestHeaders' => $requestHeaders, - 'responseHeaders' => $responseHeaders, - 'route' => Yii::$app->requestedAction ? Yii::$app->requestedAction->getUniqueId() : Yii::$app->requestedRoute, - 'action' => $action, - 'actionParams' => Yii::$app->requestedParams, - 'requestBody' => Yii::$app->getRequest()->getRawBody() == '' ? [] : [ - 'Content Type' => Yii::$app->getRequest()->getContentType(), - 'Raw' => Yii::$app->getRequest()->getRawBody(), - 'Decoded to Params' => Yii::$app->getRequest()->getBodyParams(), - ], - 'SERVER' => empty($_SERVER) ? [] : $_SERVER, - 'GET' => empty($_GET) ? [] : $_GET, - 'POST' => empty($_POST) ? [] : $_POST, - 'COOKIE' => empty($_COOKIE) ? [] : $_COOKIE, - 'FILES' => empty($_FILES) ? [] : $_FILES, - 'SESSION' => empty($_SESSION) ? [] : $_SESSION, - ]; - } + $responseHeaders = []; + foreach (headers_list() as $header) { + if (($pos = strpos($header, ':')) !== false) { + $name = substr($header, 0, $pos); + $value = trim(substr($header, $pos + 1)); + if (isset($responseHeaders[$name])) { + if (!is_array($responseHeaders[$name])) { + $responseHeaders[$name] = [$responseHeaders[$name], $value]; + } else { + $responseHeaders[$name][] = $value; + } + } else { + $responseHeaders[$name] = $value; + } + } else { + $responseHeaders[] = $header; + } + } + if (Yii::$app->requestedAction) { + if (Yii::$app->requestedAction instanceof InlineAction) { + $action = get_class(Yii::$app->requestedAction->controller) . '::' . Yii::$app->requestedAction->actionMethod . '()'; + } else { + $action = get_class(Yii::$app->requestedAction) . '::run()'; + } + } else { + $action = null; + } + /** @var \yii\web\Session $session */ + $session = Yii::$app->getComponent('session', false); + + return [ + 'flashes' => $session ? $session->getAllFlashes() : [], + 'statusCode' => Yii::$app->getResponse()->getStatusCode(), + 'requestHeaders' => $requestHeaders, + 'responseHeaders' => $responseHeaders, + 'route' => Yii::$app->requestedAction ? Yii::$app->requestedAction->getUniqueId() : Yii::$app->requestedRoute, + 'action' => $action, + 'actionParams' => Yii::$app->requestedParams, + 'requestBody' => Yii::$app->getRequest()->getRawBody() == '' ? [] : [ + 'Content Type' => Yii::$app->getRequest()->getContentType(), + 'Raw' => Yii::$app->getRequest()->getRawBody(), + 'Decoded to Params' => Yii::$app->getRequest()->getBodyParams(), + ], + 'SERVER' => empty($_SERVER) ? [] : $_SERVER, + 'GET' => empty($_GET) ? [] : $_GET, + 'POST' => empty($_POST) ? [] : $_POST, + 'COOKIE' => empty($_COOKIE) ? [] : $_COOKIE, + 'FILES' => empty($_FILES) ? [] : $_FILES, + 'SESSION' => empty($_SESSION) ? [] : $_SESSION, + ]; + } } diff --git a/extensions/debug/views/default/index.php b/extensions/debug/views/default/index.php index dc132e44757..411d9f17b45 100644 --- a/extensions/debug/views/default/index.php +++ b/extensions/debug/views/default/index.php @@ -16,107 +16,107 @@ ?>
+
+ + + getSummary() ?> + +
-
- - - getSummary() ?> - -
- -
-
+
+
context->module->panels['db']) && isset($this->context->module->panels['request'])) { - echo "

Available Debug Data

"; - $timeFormatter = extension_loaded('intl') ? Yii::createObject(['class' => 'yii\i18n\Formatter']) : Yii::$app->formatter; + echo "

Available Debug Data

"; + $timeFormatter = extension_loaded('intl') ? Yii::createObject(['class' => 'yii\i18n\Formatter']) : Yii::$app->formatter; + + echo GridView::widget([ + 'dataProvider' => $dataProvider, + 'filterModel' => $searchModel, + 'rowOptions' => function ($model, $key, $index, $grid) use ($searchModel) { + $dbPanel = $this->context->module->panels['db']; - echo GridView::widget([ - 'dataProvider' => $dataProvider, - 'filterModel' => $searchModel, - 'rowOptions' => function ($model, $key, $index, $grid) use ($searchModel) { - $dbPanel = $this->context->module->panels['db']; + if ($searchModel->isCodeCritical($model['statusCode']) || $dbPanel->isQueryCountCritical($model['sqlCount'])) { + return ['class'=>'danger']; + } else { + return []; + } + }, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + [ + 'attribute' => 'tag', + 'value' => function ($data) { + return Html::a($data['tag'], ['view', 'tag' => $data['tag']]); + }, + 'format' => 'html', + ], + [ + 'attribute' => 'time', + 'value' => function ($data) use ($timeFormatter) { + return $timeFormatter->asDateTime($data['time'], 'short'); + }, + ], + 'ip', + [ + 'attribute' => 'sqlCount', + 'label' => 'Query Count', + 'value' => function ($data) { + $dbPanel = $this->context->module->panels['db']; - if ($searchModel->isCodeCritical($model['statusCode']) || $dbPanel->isQueryCountCritical($model['sqlCount'])) { - return ['class'=>'danger']; - } else { - return []; - } - }, - 'columns' => [ - ['class' => 'yii\grid\SerialColumn'], - [ - 'attribute' => 'tag', - 'value' => function ($data) { - return Html::a($data['tag'], ['view', 'tag' => $data['tag']]); - }, - 'format' => 'html', - ], - [ - 'attribute' => 'time', - 'value' => function ($data) use ($timeFormatter) { - return $timeFormatter->asDateTime($data['time'], 'short'); - }, - ], - 'ip', - [ - 'attribute' => 'sqlCount', - 'label' => 'Query Count', - 'value' => function ($data) { - $dbPanel = $this->context->module->panels['db']; + if ($dbPanel->isQueryCountCritical($data['sqlCount'])) { - if ($dbPanel->isQueryCountCritical($data['sqlCount'])) { + $content = Html::tag('b', $data['sqlCount']) . ' ' . Html::tag('span', '', ['class' => 'glyphicon glyphicon-exclamation-sign']); - $content = Html::tag('b', $data['sqlCount']) . ' ' . Html::tag('span', '', ['class' => 'glyphicon glyphicon-exclamation-sign']); - return Html::a($content, ['view', 'panel' => 'db', 'tag' => $data['tag']], [ - 'title' => 'Too many queries. Allowed count is ' . $dbPanel->criticalQueryThreshold, - ]); + return Html::a($content, ['view', 'panel' => 'db', 'tag' => $data['tag']], [ + 'title' => 'Too many queries. Allowed count is ' . $dbPanel->criticalQueryThreshold, + ]); - } else { - return $data['sqlCount']; - } - }, - 'format' => 'html', - ], - [ - 'attribute' => 'mailCount', - 'visible' => isset($this->context->module->panels['mail']), - ], - [ - 'attribute' => 'method', - 'filter' => ['get' => 'GET', 'post' => 'POST', 'delete' => 'DELETE', 'put' => 'PUT', 'head' => 'HEAD'] - ], - [ - 'attribute'=>'ajax', - 'value' => function ($data) { - return $data['ajax'] ? 'Yes' : 'No'; - }, - 'filter' => ['No', 'Yes'], - ], - [ - 'attribute' => 'url', - 'label' => 'URL', - ], - [ - 'attribute' => 'statusCode', - 'filter' => [200 => 200, 404 => 404, 403 => 403, 500 => 500], - 'label' => 'Status code' - ], - ], - ]); + } else { + return $data['sqlCount']; + } + }, + 'format' => 'html', + ], + [ + 'attribute' => 'mailCount', + 'visible' => isset($this->context->module->panels['mail']), + ], + [ + 'attribute' => 'method', + 'filter' => ['get' => 'GET', 'post' => 'POST', 'delete' => 'DELETE', 'put' => 'PUT', 'head' => 'HEAD'] + ], + [ + 'attribute'=>'ajax', + 'value' => function ($data) { + return $data['ajax'] ? 'Yes' : 'No'; + }, + 'filter' => ['No', 'Yes'], + ], + [ + 'attribute' => 'url', + 'label' => 'URL', + ], + [ + 'attribute' => 'statusCode', + 'filter' => [200 => 200, 404 => 404, 403 => 403, 500 => 500], + 'label' => 'Status code' + ], + ], + ]); } else { - echo "
No data available. Panel db or request not found.
"; + echo "
No data available. Panel db or request not found.
"; } ?> -
-
+
+
diff --git a/extensions/debug/views/default/panels/config/detail.php b/extensions/debug/views/default/panels/config/detail.php index 11ba53f74c3..8fe865d70ca 100644 --- a/extensions/debug/views/default/panels/config/detail.php +++ b/extensions/debug/views/default/panels/config/detail.php @@ -8,30 +8,30 @@ render('panels/config/table', [ - 'caption' => 'Application Configuration', - 'values' => [ - 'Yii Version' => $panel->data['application']['yii'], - 'Application Name' => $panel->data['application']['name'], - 'Environment' => $panel->data['application']['env'], - 'Debug Mode' => $panel->data['application']['debug'] ? 'Yes' : 'No', - ], + 'caption' => 'Application Configuration', + 'values' => [ + 'Yii Version' => $panel->data['application']['yii'], + 'Application Name' => $panel->data['application']['name'], + 'Environment' => $panel->data['application']['env'], + 'Debug Mode' => $panel->data['application']['debug'] ? 'Yes' : 'No', + ], ]); if (!empty($extensions)) { - echo $this->render('panels/config/table', [ - 'caption' => 'Installed Extensions', - 'values' => $extensions, - ]); + echo $this->render('panels/config/table', [ + 'caption' => 'Installed Extensions', + 'values' => $extensions, + ]); } echo $this->render('panels/config/table', [ - 'caption' => 'PHP Configuration', - 'values' => [ - 'PHP Version' => $panel->data['php']['version'], - 'Xdebug' => $panel->data['php']['xdebug'] ? 'Enabled' : 'Disabled', - 'APC' => $panel->data['php']['apc'] ? 'Enabled' : 'Disabled', - 'Memcache' => $panel->data['php']['memcache'] ? 'Enabled' : 'Disabled', - ], + 'caption' => 'PHP Configuration', + 'values' => [ + 'PHP Version' => $panel->data['php']['version'], + 'Xdebug' => $panel->data['php']['xdebug'] ? 'Enabled' : 'Disabled', + 'APC' => $panel->data['php']['apc'] ? 'Enabled' : 'Disabled', + 'Memcache' => $panel->data['php']['memcache'] ? 'Enabled' : 'Disabled', + ], ]); echo $panel->getPhpInfo(); diff --git a/extensions/debug/views/default/panels/config/summary.php b/extensions/debug/views/default/panels/config/summary.php index bb3a6ddaf5e..d6d7b54e0f1 100644 --- a/extensions/debug/views/default/panels/config/summary.php +++ b/extensions/debug/views/default/panels/config/summary.php @@ -4,10 +4,10 @@ */ ?> diff --git a/extensions/debug/views/default/panels/config/table.php b/extensions/debug/views/default/panels/config/table.php index f5efc652182..fa4a524949e 100644 --- a/extensions/debug/views/default/panels/config/table.php +++ b/extensions/debug/views/default/panels/config/table.php @@ -11,25 +11,25 @@ -

Empty.

+

Empty.

-
- - - - - - - - $value): ?> - - - - - - -
NameValue
+ + + + + + + + + $value): ?> + + + + + + +
NameValue
diff --git a/extensions/debug/views/default/panels/db/detail.php b/extensions/debug/views/default/panels/db/detail.php index 762fdf55880..c8d8413018e 100644 --- a/extensions/debug/views/default/panels/db/detail.php +++ b/extensions/debug/views/default/panels/db/detail.php @@ -8,62 +8,64 @@ $dataProvider, - 'id' => 'db-panel-detailed-grid', - 'options' => ['class' => 'detail-grid-view'], - 'filterModel' => $searchModel, - 'filterUrl' => $panel->getUrl(), - 'columns' => [ - ['class' => 'yii\grid\SerialColumn'], - [ - 'attribute' => 'seq', - 'label' => 'Time', - 'value' => function ($data) { - $timeInSeconds = $data['timestamp'] / 1000; - $millisecondsDiff = (int)(($timeInSeconds - (int)$timeInSeconds) * 1000); - return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff); - }, - 'headerOptions' => [ - 'class' => 'sort-numerical' - ] - ], - [ - 'attribute' => 'duration', - 'value' => function ($data) { - return sprintf('%.1f ms', $data['duration']); - }, - 'options' => [ - 'width' => '10%', - ], - 'headerOptions' => [ - 'class' => 'sort-numerical' - ] - ], - [ - 'attribute' => 'type', - 'value' => function ($data) { - return Html::encode(mb_strtoupper($data['type'], 'utf8')); - }, - ], - [ - 'attribute' => 'query', - 'value' => function ($data) { - $query = Html::encode($data['query']); + 'dataProvider' => $dataProvider, + 'id' => 'db-panel-detailed-grid', + 'options' => ['class' => 'detail-grid-view'], + 'filterModel' => $searchModel, + 'filterUrl' => $panel->getUrl(), + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + [ + 'attribute' => 'seq', + 'label' => 'Time', + 'value' => function ($data) { + $timeInSeconds = $data['timestamp'] / 1000; + $millisecondsDiff = (int) (($timeInSeconds - (int) $timeInSeconds) * 1000); - if (!empty($data['trace'])) { - $query .= Html::ul($data['trace'], [ - 'class' => 'trace', - 'item' => function ($trace) { - return "
  • {$trace['file']} ({$trace['line']})
  • "; - }, - ]); - } - return $query; - }, - 'format' => 'html', - 'options' => [ - 'width' => '60%', - ], - ] - ], + return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff); + }, + 'headerOptions' => [ + 'class' => 'sort-numerical' + ] + ], + [ + 'attribute' => 'duration', + 'value' => function ($data) { + return sprintf('%.1f ms', $data['duration']); + }, + 'options' => [ + 'width' => '10%', + ], + 'headerOptions' => [ + 'class' => 'sort-numerical' + ] + ], + [ + 'attribute' => 'type', + 'value' => function ($data) { + return Html::encode(mb_strtoupper($data['type'], 'utf8')); + }, + ], + [ + 'attribute' => 'query', + 'value' => function ($data) { + $query = Html::encode($data['query']); + + if (!empty($data['trace'])) { + $query .= Html::ul($data['trace'], [ + 'class' => 'trace', + 'item' => function ($trace) { + return "
  • {$trace['file']} ({$trace['line']})
  • "; + }, + ]); + } + + return $query; + }, + 'format' => 'html', + 'options' => [ + 'width' => '60%', + ], + ] + ], ]); diff --git a/extensions/debug/views/default/panels/db/summary.php b/extensions/debug/views/default/panels/db/summary.php index 022f9a759b6..72d203e4ce6 100644 --- a/extensions/debug/views/default/panels/db/summary.php +++ b/extensions/debug/views/default/panels/db/summary.php @@ -1,7 +1,7 @@ diff --git a/extensions/debug/views/default/panels/log/detail.php b/extensions/debug/views/default/panels/log/detail.php index 5f9d4a19282..aad97897c6c 100644 --- a/extensions/debug/views/default/panels/log/detail.php +++ b/extensions/debug/views/default/panels/log/detail.php @@ -8,65 +8,66 @@ $dataProvider, - 'id' => 'log-panel-detailed-grid', - 'options' => ['class' => 'detail-grid-view'], - 'filterModel' => $searchModel, - 'filterUrl' => $panel->getUrl(), - 'rowOptions' => function ($model, $key, $index, $grid) { - switch($model['level']) { - case Logger::LEVEL_ERROR : return ['class' => 'danger']; - case Logger::LEVEL_WARNING : return ['class' => 'warning']; - case Logger::LEVEL_INFO : return ['class' => 'success']; - default: return []; - } - }, - 'columns' => [ - ['class' => 'yii\grid\SerialColumn'], - [ - 'attribute' => 'time', - 'value' => function ($data) { - $timeInSeconds = $data['time'] / 1000; - $millisecondsDiff = (int)(($timeInSeconds - (int)$timeInSeconds) * 1000); - return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff); - }, - 'headerOptions' => [ - 'class' => 'sort-numerical' - ] - ], - [ - 'attribute' => 'level', - 'value' => function ($data) { - return Logger::getLevelName($data['level']); - }, - 'filter' => [ - Logger::LEVEL_TRACE => ' Trace ', - Logger::LEVEL_INFO => ' Info ', - Logger::LEVEL_WARNING => ' Warning ', - Logger::LEVEL_ERROR => ' Error ', - ], - ], - 'category', - [ - 'attribute' => 'message', - 'value' => function ($data) { - $message = nl2br(Html::encode($data['message'])); + 'dataProvider' => $dataProvider, + 'id' => 'log-panel-detailed-grid', + 'options' => ['class' => 'detail-grid-view'], + 'filterModel' => $searchModel, + 'filterUrl' => $panel->getUrl(), + 'rowOptions' => function ($model, $key, $index, $grid) { + switch ($model['level']) { + case Logger::LEVEL_ERROR : return ['class' => 'danger']; + case Logger::LEVEL_WARNING : return ['class' => 'warning']; + case Logger::LEVEL_INFO : return ['class' => 'success']; + default: return []; + } + }, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + [ + 'attribute' => 'time', + 'value' => function ($data) { + $timeInSeconds = $data['time'] / 1000; + $millisecondsDiff = (int) (($timeInSeconds - (int) $timeInSeconds) * 1000); - if (!empty($data['trace'])) { - $message .= Html::ul($data['trace'], [ - 'class' => 'trace', - 'item' => function ($trace) { - return "
  • {$trace['file']} ({$trace['line']})
  • "; - } - ]); - }; + return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff); + }, + 'headerOptions' => [ + 'class' => 'sort-numerical' + ] + ], + [ + 'attribute' => 'level', + 'value' => function ($data) { + return Logger::getLevelName($data['level']); + }, + 'filter' => [ + Logger::LEVEL_TRACE => ' Trace ', + Logger::LEVEL_INFO => ' Info ', + Logger::LEVEL_WARNING => ' Warning ', + Logger::LEVEL_ERROR => ' Error ', + ], + ], + 'category', + [ + 'attribute' => 'message', + 'value' => function ($data) { + $message = nl2br(Html::encode($data['message'])); - return $message; - }, - 'format' => 'html', - 'options' => [ - 'width' => '50%', - ], - ], - ], + if (!empty($data['trace'])) { + $message .= Html::ul($data['trace'], [ + 'class' => 'trace', + 'item' => function ($trace) { + return "
  • {$trace['file']} ({$trace['line']})
  • "; + } + ]); + }; + + return $message; + }, + 'format' => 'html', + 'options' => [ + 'width' => '50%', + ], + ], + ], ]); diff --git a/extensions/debug/views/default/panels/log/summary.php b/extensions/debug/views/default/panels/log/summary.php index 692679d0e94..86bcaaece2e 100644 --- a/extensions/debug/views/default/panels/log/summary.php +++ b/extensions/debug/views/default/panels/log/summary.php @@ -11,19 +11,19 @@ $output = []; if ($errorCount) { - $output[] = "$errorCount"; - $title .= ", $errorCount errors"; + $output[] = "$errorCount"; + $title .= ", $errorCount errors"; } if ($warningCount) { - $output[] = "$warningCount"; - $title .= ", $warningCount warnings"; + $output[] = "$warningCount"; + $title .= ", $warningCount warnings"; } ?> diff --git a/extensions/debug/views/default/panels/mail/_item.php b/extensions/debug/views/default/panels/mail/_item.php index fd485c96eb4..a58ebd99be6 100644 --- a/extensions/debug/views/default/panels/mail/_item.php +++ b/extensions/debug/views/default/panels/mail/_item.php @@ -6,33 +6,33 @@ $timeFormatter = extension_loaded('intl') ? Yii::createObject(['class' => 'yii\i18n\Formatter']) : Yii::$app->formatter; echo DetailView::widget([ - 'model' => $model, - 'attributes' => [ - 'headers', - 'from', - 'to', - 'charset', - [ - 'name' => 'time', - 'value' => $timeFormatter->asDateTime($model['time'], 'short'), - ], - 'subject', - [ - 'name' => 'body', - 'label' => 'Text body', - ], - [ - 'name' => 'isSuccessful', - 'label' => 'Successfully sent', - 'value' => $model['isSuccessful'] ? 'Yes' : 'No' - ], - 'reply', - 'bcc', - 'cc', - [ - 'name' => 'file', - 'format' => 'html', - 'value' => Html::a('Download eml', ['download-mail', 'file' => $model['file']]), - ], - ], + 'model' => $model, + 'attributes' => [ + 'headers', + 'from', + 'to', + 'charset', + [ + 'name' => 'time', + 'value' => $timeFormatter->asDateTime($model['time'], 'short'), + ], + 'subject', + [ + 'name' => 'body', + 'label' => 'Text body', + ], + [ + 'name' => 'isSuccessful', + 'label' => 'Successfully sent', + 'value' => $model['isSuccessful'] ? 'Yes' : 'No' + ], + 'reply', + 'bcc', + 'cc', + [ + 'name' => 'file', + 'format' => 'html', + 'value' => Html::a('Download eml', ['download-mail', 'file' => $model['file']]), + ], + ], ]); diff --git a/extensions/debug/views/default/panels/mail/detail.php b/extensions/debug/views/default/panels/mail/detail.php index 59d0d13d580..bc41eec6f7e 100644 --- a/extensions/debug/views/default/panels/mail/detail.php +++ b/extensions/debug/views/default/panels/mail/detail.php @@ -4,52 +4,52 @@ use yii\helpers\Html; $listView = new ListView([ - 'dataProvider' => $dataProvider, - 'itemView' => 'panels/mail/_item', - 'layout' => "{summary}\n{items}\n{pager}\n", - ]); + 'dataProvider' => $dataProvider, + 'itemView' => 'panels/mail/_item', + 'layout' => "{summary}\n{items}\n{pager}\n", + ]); $listView->sorter = ['options' => ['class'=>'mail-sorter']]; ?>

    Email messages

    -
    - 'btn btn-default', 'onclick'=>'$("#email-form").toggle();']) ?> -
    -
    - renderSorter() ?> -
    +
    + 'btn btn-default', 'onclick'=>'$("#email-form").toggle();']) ?> +
    +
    + renderSorter() ?> +
    run() ?> diff --git a/extensions/debug/views/default/panels/mail/summary.php b/extensions/debug/views/default/panels/mail/summary.php index 4e35ff8a22e..0ed37102467 100644 --- a/extensions/debug/views/default/panels/mail/summary.php +++ b/extensions/debug/views/default/panels/mail/summary.php @@ -4,6 +4,6 @@ */ if ($mailCount): ?> diff --git a/extensions/debug/views/default/panels/profile/detail.php b/extensions/debug/views/default/panels/profile/detail.php index f0d4a660666..a9bbe7ec94d 100644 --- a/extensions/debug/views/default/panels/profile/detail.php +++ b/extensions/debug/views/default/panels/profile/detail.php @@ -7,47 +7,48 @@

    Total processing time: ; Peak memory: .

    $dataProvider, - 'id' => 'profile-panel-detailed-grid', - 'options' => ['class' => 'detail-grid-view'], - 'filterModel' => $searchModel, - 'filterUrl' => $panel->getUrl(), - 'columns' => [ - ['class' => 'yii\grid\SerialColumn'], - [ - 'attribute' => 'seq', - 'label' => 'Time', - 'value' => function ($data) { - $timeInSeconds = $data['timestamp'] / 1000; - $millisecondsDiff = (int)(($timeInSeconds - (int)$timeInSeconds) * 1000); - return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff); - }, - 'headerOptions' => [ - 'class' => 'sort-numerical' - ] - ], - [ - 'attribute' => 'duration', - 'value' => function ($data) { - return sprintf('%.1f ms', $data['duration']); - }, - 'options' => [ - 'width' => '10%', - ], - 'headerOptions' => [ - 'class' => 'sort-numerical' - ] - ], - 'category', - [ - 'attribute' => 'info', - 'value' => function ($data) { - return str_repeat('', $data['level']) . Html::encode($data['info']); - }, - 'format' => 'html', - 'options' => [ - 'width' => '60%', - ], - ], - ], + 'dataProvider' => $dataProvider, + 'id' => 'profile-panel-detailed-grid', + 'options' => ['class' => 'detail-grid-view'], + 'filterModel' => $searchModel, + 'filterUrl' => $panel->getUrl(), + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + [ + 'attribute' => 'seq', + 'label' => 'Time', + 'value' => function ($data) { + $timeInSeconds = $data['timestamp'] / 1000; + $millisecondsDiff = (int) (($timeInSeconds - (int) $timeInSeconds) * 1000); + + return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff); + }, + 'headerOptions' => [ + 'class' => 'sort-numerical' + ] + ], + [ + 'attribute' => 'duration', + 'value' => function ($data) { + return sprintf('%.1f ms', $data['duration']); + }, + 'options' => [ + 'width' => '10%', + ], + 'headerOptions' => [ + 'class' => 'sort-numerical' + ] + ], + 'category', + [ + 'attribute' => 'info', + 'value' => function ($data) { + return str_repeat('', $data['level']) . Html::encode($data['info']); + }, + 'format' => 'html', + 'options' => [ + 'width' => '60%', + ], + ], + ], ]); diff --git a/extensions/debug/views/default/panels/profile/summary.php b/extensions/debug/views/default/panels/profile/summary.php index 52b8e88aeb6..ae6d34bd7dd 100644 --- a/extensions/debug/views/default/panels/profile/summary.php +++ b/extensions/debug/views/default/panels/profile/summary.php @@ -1,4 +1,4 @@ diff --git a/extensions/debug/views/default/panels/request/detail.php b/extensions/debug/views/default/panels/request/detail.php index 9f5458086cd..2dd1312267a 100644 --- a/extensions/debug/views/default/panels/request/detail.php +++ b/extensions/debug/views/default/panels/request/detail.php @@ -1,4 +1,4 @@ -Request"; echo Tabs::widget([ - 'items' => [ - [ - 'label' => 'Parameters', - 'content' => $this->render('panels/request/table', ['caption' => 'Routing', 'values' => ['Route' => $panel->data['route'], 'Action' => $panel->data['action'], 'Parameters' => $panel->data['actionParams']]]) - . $this->render('panels/request/table', ['caption' => '$_GET', 'values' => $panel->data['GET']]) - . $this->render('panels/request/table', ['caption' => '$_POST', 'values' => $panel->data['POST']]) - . $this->render('panels/request/table', ['caption' => '$_FILES', 'values' => $panel->data['FILES']]) - . $this->render('panels/request/table', ['caption' => '$_COOKIE', 'values' => $panel->data['COOKIE']]) - . $this->render('panels/request/table', ['caption' => 'Request Body', 'values' => $panel->data['requestBody']]), - 'active' => true, - ], - [ - 'label' => 'Headers', - 'content' => $this->render('panels/request/table', ['caption' => 'Request Headers', 'values' => $panel->data['requestHeaders']]) - . $this->render('panels/request/table', ['caption' => 'Response Headers', 'values' => $panel->data['responseHeaders']]) - ], - [ - 'label' => 'Session', - 'content' => $this->render('panels/request/table', ['caption' => '$_SESSION', 'values' => $panel->data['SESSION']]) - . $this->render('panels/request/table', ['caption' => 'Flashes', 'values' => $panel->data['flashes']]) - ], - [ - 'label' => '$_SERVER', - 'content' => $this->render('panels/request/table', ['caption' => '$_SERVER', 'values' => $panel->data['SERVER']]), - ], - ], + 'items' => [ + [ + 'label' => 'Parameters', + 'content' => $this->render('panels/request/table', ['caption' => 'Routing', 'values' => ['Route' => $panel->data['route'], 'Action' => $panel->data['action'], 'Parameters' => $panel->data['actionParams']]]) + . $this->render('panels/request/table', ['caption' => '$_GET', 'values' => $panel->data['GET']]) + . $this->render('panels/request/table', ['caption' => '$_POST', 'values' => $panel->data['POST']]) + . $this->render('panels/request/table', ['caption' => '$_FILES', 'values' => $panel->data['FILES']]) + . $this->render('panels/request/table', ['caption' => '$_COOKIE', 'values' => $panel->data['COOKIE']]) + . $this->render('panels/request/table', ['caption' => 'Request Body', 'values' => $panel->data['requestBody']]), + 'active' => true, + ], + [ + 'label' => 'Headers', + 'content' => $this->render('panels/request/table', ['caption' => 'Request Headers', 'values' => $panel->data['requestHeaders']]) + . $this->render('panels/request/table', ['caption' => 'Response Headers', 'values' => $panel->data['responseHeaders']]) + ], + [ + 'label' => 'Session', + 'content' => $this->render('panels/request/table', ['caption' => '$_SESSION', 'values' => $panel->data['SESSION']]) + . $this->render('panels/request/table', ['caption' => 'Flashes', 'values' => $panel->data['flashes']]) + ], + [ + 'label' => '$_SERVER', + 'content' => $this->render('panels/request/table', ['caption' => '$_SERVER', 'values' => $panel->data['SERVER']]), + ], + ], ]); diff --git a/extensions/debug/views/default/panels/request/summary.php b/extensions/debug/views/default/panels/request/summary.php index a80ba558fcd..cc1bbd72895 100644 --- a/extensions/debug/views/default/panels/request/summary.php +++ b/extensions/debug/views/default/panels/request/summary.php @@ -8,18 +8,18 @@ $statusCode = $panel->data['statusCode']; if ($statusCode === null) { - $statusCode = 200; + $statusCode = 200; } if ($statusCode >= 200 && $statusCode < 300) { - $class = 'label-success'; + $class = 'label-success'; } elseif ($statusCode >= 100 && $statusCode < 200) { - $class = 'label-info'; + $class = 'label-info'; } else { - $class = 'label-important'; + $class = 'label-important'; } $statusText = Html::encode(isset(Response::$httpStatuses[$statusCode]) ? Response::$httpStatuses[$statusCode] : ''); ?> diff --git a/extensions/debug/views/default/panels/request/table.php b/extensions/debug/views/default/panels/request/table.php index 333ede137f8..2045bfac7be 100644 --- a/extensions/debug/views/default/panels/request/table.php +++ b/extensions/debug/views/default/panels/request/table.php @@ -11,25 +11,25 @@ -

    Empty.

    +

    Empty.

    - - - - - - - - - $value): ?> - - - - - - -
    NameValue
    charset, true) ?>
    + + + + + + + + + $value): ?> + + + + + + +
    NameValue
    charset, true) ?>
    diff --git a/extensions/debug/views/default/toolbar.php b/extensions/debug/views/default/toolbar.php index 0dd3a7dd676..3520f2e23a6 100644 --- a/extensions/debug/views/default/toolbar.php +++ b/extensions/debug/views/default/toolbar.php @@ -11,7 +11,7 @@ document.getElementById('yii-debug-toolbar').style.display = 'none'; document.getElementById('yii-debug-toolbar-min').style.display = 'block'; if (window.localStorage) { - localStorage.setItem('yii-debug-toolbar', 'minimized'); + localStorage.setItem('yii-debug-toolbar', 'minimized'); } EOD; @@ -19,7 +19,7 @@ document.getElementById('yii-debug-toolbar-min').style.display = 'none'; document.getElementById('yii-debug-toolbar').style.display = 'block'; if (window.localStorage) { - localStorage.setItem('yii-debug-toolbar', 'maximized'); + localStorage.setItem('yii-debug-toolbar', 'maximized'); } EOD; @@ -34,14 +34,14 @@ - - getSummary() ?> - - + + getSummary() ?> + +
    - - + +
    diff --git a/extensions/debug/views/default/view.php b/extensions/debug/views/default/view.php index 53dd25bd9bb..6cb9eaab115 100644 --- a/extensions/debug/views/default/view.php +++ b/extensions/debug/views/default/view.php @@ -17,7 +17,7 @@ $this->title = 'Yii Debugger'; ?>
    -
    +
    - - getSummary() ?> - -
    + + getSummary() ?> + +
    -
    -
    -
    -
    - $panel) { - $label = '' . Html::encode($panel->getName()); - echo Html::a($label, ['view', 'tag' => $tag, 'panel' => $id], [ - 'class' => $panel === $activePanel ? 'list-group-item active' : 'list-group-item', - ]); - } - ?> -
    -
    -
    -
    - $meta['tag'], 'panel' => $activePanel->id]; - $items[] = [ - 'label' => $label, - 'url' => $url, - ]; - if (++$count >= 10) { - break; - } - } - echo ButtonGroup::widget([ - 'buttons' => [ - Html::a('All', ['index'], ['class' => 'btn btn-default']), - ButtonDropdown::widget([ - 'label' => 'Last 10', - 'options' => ['class' => 'btn-default'], - 'dropdown' => ['items' => $items], - ]), - ], - ]); - echo "\n" . $summary['tag'] . ': ' . $summary['method'] . ' ' . Html::a(Html::encode($summary['url']), $summary['url']); - echo ' at ' . date('Y-m-d h:i:s a', $summary['time']) . ' by ' . $summary['ip']; - ?> -
    - getDetail() ?> -
    -
    -
    +
    +
    +
    +
    + $panel) { + $label = '' . Html::encode($panel->getName()); + echo Html::a($label, ['view', 'tag' => $tag, 'panel' => $id], [ + 'class' => $panel === $activePanel ? 'list-group-item active' : 'list-group-item', + ]); + } + ?> +
    +
    +
    +
    + $meta['tag'], 'panel' => $activePanel->id]; + $items[] = [ + 'label' => $label, + 'url' => $url, + ]; + if (++$count >= 10) { + break; + } + } + echo ButtonGroup::widget([ + 'buttons' => [ + Html::a('All', ['index'], ['class' => 'btn btn-default']), + ButtonDropdown::widget([ + 'label' => 'Last 10', + 'options' => ['class' => 'btn-default'], + 'dropdown' => ['items' => $items], + ]), + ], + ]); + echo "\n" . $summary['tag'] . ': ' . $summary['method'] . ' ' . Html::a(Html::encode($summary['url']), $summary['url']); + echo ' at ' . date('Y-m-d h:i:s a', $summary['time']) . ' by ' . $summary['ip']; + ?> +
    + getDetail() ?> +
    +
    +
    diff --git a/extensions/debug/views/layouts/main.php b/extensions/debug/views/layouts/main.php index b3a751f626f..1b3043ed52a 100644 --- a/extensions/debug/views/layouts/main.php +++ b/extensions/debug/views/layouts/main.php @@ -11,10 +11,10 @@ - - - <?= Html::encode($this->title) ?> - head() ?> + + + <?= Html::encode($this->title) ?> + head() ?> beginBody() ?> diff --git a/extensions/elasticsearch/ActiveQuery.php b/extensions/elasticsearch/ActiveQuery.php index 0ed64aa25a9..f2d9be773db 100644 --- a/extensions/elasticsearch/ActiveQuery.php +++ b/extensions/elasticsearch/ActiveQuery.php @@ -74,213 +74,220 @@ */ class ActiveQuery extends Query implements ActiveQueryInterface { - use ActiveQueryTrait; - use ActiveRelationTrait; + use ActiveQueryTrait; + use ActiveRelationTrait; - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($this->primaryModel !== null) { - // lazy loading - if (is_array($this->via)) { - // via relation - /** @var ActiveQuery $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - } + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + if ($this->primaryModel !== null) { + // lazy loading + if (is_array($this->via)) { + // via relation + /** @var ActiveQuery $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); + } else { + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; + } + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); + } + } - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } - if ($this->type === null) { - $this->type = $modelClass::type(); - } - if ($this->index === null) { - $this->index = $modelClass::index(); - $this->type = $modelClass::type(); - } - $commandConfig = $db->getQueryBuilder()->build($this); - return $db->createCommand($commandConfig); - } + if ($this->type === null) { + $this->type = $modelClass::type(); + } + if ($this->index === null) { + $this->index = $modelClass::index(); + $this->type = $modelClass::type(); + } + $commandConfig = $db->getQueryBuilder()->build($this); - /** - * Executes query and returns all results as an array. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $result = $this->createCommand($db)->search(); - if (empty($result['hits']['hits'])) { - return []; - } - if ($this->fields !== null) { - foreach ($result['hits']['hits'] as &$row) { - $row['_source'] = isset($row['fields']) ? $row['fields'] : []; - unset($row['fields']); - } - unset($row); - } - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - $pk = $modelClass::primaryKey()[0]; - if ($this->asArray && $this->indexBy) { - foreach ($result['hits']['hits'] as &$row) { - if ($pk === '_id') { - $row['_source']['_id'] = $row['_id']; - } - $row['_source']['_score'] = $row['_score']; - $row = $row['_source']; - } - unset($row); - } - $models = $this->createModels($result['hits']['hits']); - if ($this->asArray && !$this->indexBy) { - foreach ($models as $key => $model) { - if ($pk === '_id') { - $model['_source']['_id'] = $model['_id']; - } - $model['_source']['_score'] = $model['_score']; - $models[$key] = $model['_source']; - } - } - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - return $models; - } + return $db->createCommand($commandConfig); + } - /** - * Executes query and returns a single row of result. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - if (($result = parent::one($db)) === false) { - return null; - } - if ($this->asArray) { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - $model = $result['_source']; - $pk = $modelClass::primaryKey()[0]; - if ($pk === '_id') { - $model['_id'] = $result['_id']; - } - $model['_score'] = $result['_score']; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::instantiate($result); - $class::populateRecord($model, $result); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - if (!$this->asArray) { - $model->afterFind(); - } - return $model; - } + /** + * Executes query and returns all results as an array. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $result = $this->createCommand($db)->search(); + if (empty($result['hits']['hits'])) { + return []; + } + if ($this->fields !== null) { + foreach ($result['hits']['hits'] as &$row) { + $row['_source'] = isset($row['fields']) ? $row['fields'] : []; + unset($row['fields']); + } + unset($row); + } + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + $pk = $modelClass::primaryKey()[0]; + if ($this->asArray && $this->indexBy) { + foreach ($result['hits']['hits'] as &$row) { + if ($pk === '_id') { + $row['_source']['_id'] = $row['_id']; + } + $row['_source']['_score'] = $row['_score']; + $row = $row['_source']; + } + unset($row); + } + $models = $this->createModels($result['hits']['hits']); + if ($this->asArray && !$this->indexBy) { + foreach ($models as $key => $model) { + if ($pk === '_id') { + $model['_source']['_id'] = $model['_id']; + } + $model['_source']['_score'] = $model['_score']; + $models[$key] = $model['_source']; + } + } + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } - /** - * @inheritdoc - */ - public function search($db = null, $options = []) - { - $result = $this->createCommand($db)->search($options); - if (!empty($result['hits']['hits'])) { - $models = $this->createModels($result['hits']['hits']); - if ($this->asArray) { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - $pk = $modelClass::primaryKey()[0]; - foreach ($models as $key => $model) { - if ($pk === '_id') { - $model['_source']['_id'] = $model['_id']; - } - $model['_source']['_score'] = $model['_score']; - $models[$key] = $model['_source']; - } - } - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - $result['hits']['hits'] = $models; - } - return $result; - } + return $models; + } - /** - * @inheritdoc - */ - public function scalar($field, $db = null) - { - $record = parent::one($db); - if ($record !== false) { - if ($field == '_id') { - return $record['_id']; - } elseif (isset($record['_source'][$field])) { - return $record['_source'][$field]; - } - } - return null; - } + /** + * Executes query and returns a single row of result. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + if (($result = parent::one($db)) === false) { + return null; + } + if ($this->asArray) { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + $model = $result['_source']; + $pk = $modelClass::primaryKey()[0]; + if ($pk === '_id') { + $model['_id'] = $result['_id']; + } + $model['_score'] = $result['_score']; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::instantiate($result); + $class::populateRecord($model, $result); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + if (!$this->asArray) { + $model->afterFind(); + } - /** - * @inheritdoc - */ - public function column($field, $db = null) - { - if ($field == '_id') { - $command = $this->createCommand($db); - $command->queryParts['fields'] = []; - $result = $command->search(); - if (empty($result['hits']['hits'])) { - return []; - } - $column = []; - foreach ($result['hits']['hits'] as $row) { - $column[] = $row['_id']; - } - return $column; - } - return parent::column($field, $db); - } + return $model; + } + + /** + * @inheritdoc + */ + public function search($db = null, $options = []) + { + $result = $this->createCommand($db)->search($options); + if (!empty($result['hits']['hits'])) { + $models = $this->createModels($result['hits']['hits']); + if ($this->asArray) { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + $pk = $modelClass::primaryKey()[0]; + foreach ($models as $key => $model) { + if ($pk === '_id') { + $model['_source']['_id'] = $model['_id']; + } + $model['_source']['_score'] = $model['_score']; + $models[$key] = $model['_source']; + } + } + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } + $result['hits']['hits'] = $models; + } + + return $result; + } + + /** + * @inheritdoc + */ + public function scalar($field, $db = null) + { + $record = parent::one($db); + if ($record !== false) { + if ($field == '_id') { + return $record['_id']; + } elseif (isset($record['_source'][$field])) { + return $record['_source'][$field]; + } + } + + return null; + } + + /** + * @inheritdoc + */ + public function column($field, $db = null) + { + if ($field == '_id') { + $command = $this->createCommand($db); + $command->queryParts['fields'] = []; + $result = $command->search(); + if (empty($result['hits']['hits'])) { + return []; + } + $column = []; + foreach ($result['hits']['hits'] as $row) { + $column[] = $row['_id']; + } + + return $column; + } + + return parent::column($field, $db); + } } diff --git a/extensions/elasticsearch/ActiveRecord.php b/extensions/elasticsearch/ActiveRecord.php index bd956f809c6..75bb515773e 100644 --- a/extensions/elasticsearch/ActiveRecord.php +++ b/extensions/elasticsearch/ActiveRecord.php @@ -47,527 +47,537 @@ */ class ActiveRecord extends BaseActiveRecord { - private $_id; - private $_score; - private $_version; - - /** - * Returns the database connection used by this AR class. - * By default, the "elasticsearch" application component is used as the database connection. - * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->getComponent('elasticsearch'); - } - - /** - * @inheritdoc - */ - public static function find($q = null) - { - $query = static::createQuery(); - if (is_array($q)) { - return $query->andWhere($q)->one(); - } elseif ($q !== null) { - return static::get($q); - } - return $query; - } - - /** - * Gets a record by its primary key. - * - * @param mixed $primaryKey the primaryKey value - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. - * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) - * for more details on these options. - * @return static|null The record instance or null if it was not found. - */ - public static function get($primaryKey, $options = []) - { - if ($primaryKey === null) { - return null; - } - $command = static::getDb()->createCommand(); - $result = $command->get(static::index(), static::type(), $primaryKey, $options); - if ($result['exists']) { - $model = static::instantiate($result); - static::populateRecord($model, $result); - $model->afterFind(); - return $model; - } - return null; - } - - /** - * Gets a list of records by its primary keys. - * - * @param array $primaryKeys an array of primaryKey values - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. - * - * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) - * for more details on these options. - * @return static|null The record instance or null if it was not found. - */ - - public static function mget($primaryKeys, $options = []) - { - if (empty($primaryKeys)) { - return []; - } - $command = static::getDb()->createCommand(); - $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); - $models = []; - foreach ($result['docs'] as $doc) { - if ($doc['exists']) { - $model = static::instantiate($doc); - static::populateRecord($model, $doc); - $model->afterFind(); - $models[] = $model; - } - } - return $models; - } - - // TODO add more like this feature http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-more-like-this.html - - // TODO add percolate functionality http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-percolate.html - - /** - * Creates an [[ActiveQuery]] instance. - * - * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also - * by [[hasOne()]] and [[hasMany()]] to create a relational query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ActiveQuery($config); - } - - // TODO implement copy and move as pk change is not possible - - /** - * @return float returns the score of this record when it was retrieved via a [[find()]] query. - */ - public function getScore() - { - return $this->_score; - } - - /** - * Sets the primary key - * @param mixed $value - * @throws \yii\base\InvalidCallException when record is not new - */ - public function setPrimaryKey($value) - { - $pk = static::primaryKey()[0]; - if ($this->getIsNewRecord() || $pk != '_id') { - $this->$pk = $value; - } else { - throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); - } - } - - /** - * @inheritdoc - */ - public function getPrimaryKey($asArray = false) - { - $pk = static::primaryKey()[0]; - if ($asArray) { - return [$pk => $this->$pk]; - } else { - return $this->$pk; - } - } - - /** - * @inheritdoc - */ - public function getOldPrimaryKey($asArray = false) - { - $pk = static::primaryKey()[0]; - if ($this->getIsNewRecord()) { - $id = null; - } elseif ($pk == '_id') { - $id = $this->_id; - } else { - $id = $this->getOldAttribute($pk); - } - if ($asArray) { - return [$pk => $id]; - } else { - return $id; - } - } - - /** - * This method defines the attribute that uniquely identifies a record. - * - * The primaryKey for elasticsearch documents is the `_id` field by default. This field is not part of the - * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]]. - * - * You may overide this method to define the primary key name when you have defined - * [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) - * for the `_id` field so that it is part of the `_source` and thus part of the [[attributes()|attributes]]. - * - * Note that elasticsearch only supports _one_ attribute to be the primary key. However to match the signature - * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a - * single string. - * - * @return string[] array of primary key attributes. Only the first element of the array will be used. - */ - public static function primaryKey() - { - return ['_id']; - } - - /** - * Returns the list of all attribute names of the model. - * - * This method must be overridden by child classes to define available attributes. - * - * Attributes are names of fields of the corresponding elasticsearch document. - * The primaryKey for elasticsearch documents is the `_id` field by default which is not part of the attributes. - * You may define [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) - * for the `_id` field so that it is part of the `_source` fields and thus becomes part of the attributes. - * - * @return string[] list of attribute names. - */ - public function attributes() - { - throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); - } - - /** - * @return string the name of the index this record is stored in. - */ - public static function index() - { - return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); - } - - /** - * @return string the name of the type of this record. - */ - public static function type() - { - return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); - } - - /** - * @inheritdoc - */ - public static function populateRecord($record, $row) - { - parent::populateRecord($record, $row['_source']); - $pk = static::primaryKey()[0]; - if ($pk === '_id') { - $record->_id = $row['_id']; - } - $record->_score = isset($row['_score']) ? $row['_score'] : null; - $record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available... - } - - /** - * Creates an active record instance. - * - * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. - * It is not meant to be used for creating new records directly. - * - * You may override this method if the instance being created - * depends on the row data to be populated into the record. - * For example, by creating a record based on the value of a column, - * you may implement the so-called single-table inheritance mapping. - * @param array $row row data to be populated into the record. - * This array consists of the following keys: - * - `_source`: refers to the attributes of the record. - * - `_type`: the type this record is stored in. - * - `_index`: the index this record is stored in. - * @return static the newly created active record - */ - public static function instantiate($row) - { - return new static; - } - - /** - * Inserts a document into the associated index using the attribute values of this record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. insert the record into database. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. - * - * If the [[primaryKey|primary key]] is not set (null) during insertion, - * it will be populated with a - * [randomly generated value](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) - * after insertion. - * - * For example, to insert a customer record: - * - * ~~~ - * $customer = new Customer; - * $customer->name = $name; - * $customer->email = $email; - * $customer->insert(); - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes will be saved. - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. These are among others: - * - * - `routing` define shard placement of this record. - * - `parent` by giving the primaryKey of another record this defines a parent-child relation - * - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. - * - * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) - * for more details on these options. - * - * By default the `op_type` is set to `create`. - * @return boolean whether the attributes are valid and the record is inserted successfully. - */ - public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - if ($this->beforeSave(true)) { - $values = $this->getDirtyAttributes($attributes); - - $response = static::getDb()->createCommand()->insert( - static::index(), - static::type(), - $values, - $this->getPrimaryKey(), - $options - ); - - if (!isset($response['ok'])) { - return false; - } - $pk = static::primaryKey()[0]; - $this->$pk = $response['_id']; - if ($pk != '_id') { - $values[$pk] = $response['_id']; - } - $this->_version = $response['_version']; - $this->_score = null; - $this->setOldAttributes($values); - $this->afterSave(true); - return true; - } - return false; - } - - /** - * Updates all records whos primary keys are given. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], [2, 3, 4]); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = []) - { - $pkName = static::primaryKey()[0]; - if (count($condition) == 1 && isset($condition[$pkName])) { - $primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]]; - } else { - $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id - } - if (empty($primaryKeys)) { - return 0; - } - $bulk = ''; - foreach ($primaryKeys as $pk) { - $action = Json::encode([ - "update" => [ - "_id" => $pk, - "_type" => static::type(), - "_index" => static::index(), - ], - ]); - $data = Json::encode([ - "doc" => $attributes - ]); - $bulk .= $action . "\n" . $data . "\n"; - } - - // TODO do this via command - $url = [static::index(), static::type(), '_bulk']; - $response = static::getDb()->post($url, [], $bulk); - $n = 0; - $errors = []; - foreach ($response['items'] as $item) { - if (isset($item['update']['error'])) { - $errors[] = $item['update']; - } elseif ($item['update']['ok']) { - $n++; - } - } - if (!empty($errors)) { - throw new Exception(__METHOD__ . ' failed updating records.', $errors); - } - return $n; - } - - /** - * Updates all matching records using the provided counter changes and conditions. - * For example, to increment all customers' age by 1, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @return integer the number of rows updated - */ - public static function updateAllCounters($counters, $condition = []) - { - $pkName = static::primaryKey()[0]; - if (count($condition) == 1 && isset($condition[$pkName])) { - $primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]]; - } else { - $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id - } - if (empty($primaryKeys) || empty($counters)) { - return 0; - } - $bulk = ''; - foreach ($primaryKeys as $pk) { - $action = Json::encode([ - "update" => [ - "_id" => $pk, - "_type" => static::type(), - "_index" => static::index(), - ], - ]); - $script = ''; - foreach ($counters as $counter => $value) { - $script .= "ctx._source.$counter += $counter;\n"; - } - $data = Json::encode([ - "script" => $script, - "params" => $counters - ]); - $bulk .= $action . "\n" . $data . "\n"; - } - - // TODO do this via command - $url = [static::index(), static::type(), '_bulk']; - $response = static::getDb()->post($url, [], $bulk); - $n = 0; - $errors = []; - foreach ($response['items'] as $item) { - if (isset($item['update']['error'])) { - $errors[] = $item['update']; - } elseif ($item['update']['ok']) { - $n++; - } - } - if (!empty($errors)) { - throw new Exception(__METHOD__ . ' failed updating records counters.', $errors); - } - return $n; - } - - /** - * Deletes rows in the table using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll('status = 3'); - * ~~~ - * - * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = []) - { - $pkName = static::primaryKey()[0]; - if (count($condition) == 1 && isset($condition[$pkName])) { - $primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]]; - } else { - $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id - } - if (empty($primaryKeys)) { - return 0; - } - $bulk = ''; - foreach ($primaryKeys as $pk) { - $bulk .= Json::encode([ - "delete" => [ - "_id" => $pk, - "_type" => static::type(), - "_index" => static::index(), - ], - ]) . "\n"; - } - - // TODO do this via command - $url = [static::index(), static::type(), '_bulk']; - $response = static::getDb()->post($url, [], $bulk); - $n = 0; - $errors = []; - foreach ($response['items'] as $item) { - if (isset($item['delete']['error'])) { - $errors[] = $item['delete']; - } elseif ($item['delete']['found'] && $item['delete']['ok']) { - $n++; - } - } - if (!empty($errors)) { - throw new Exception(__METHOD__ . ' failed deleting records.', $errors); - } - return $n; - } + private $_id; + private $_score; + private $_version; + + /** + * Returns the database connection used by this AR class. + * By default, the "elasticsearch" application component is used as the database connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getComponent('elasticsearch'); + } + + /** + * @inheritdoc + */ + public static function find($q = null) + { + $query = static::createQuery(); + if (is_array($q)) { + return $query->andWhere($q)->one(); + } elseif ($q !== null) { + return static::get($q); + } + + return $query; + } + + /** + * Gets a record by its primary key. + * + * @param mixed $primaryKey the primaryKey value + * @param array $options options given in this parameter are passed to elasticsearch + * as request URI parameters. + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) + * for more details on these options. + * @return static|null The record instance or null if it was not found. + */ + public static function get($primaryKey, $options = []) + { + if ($primaryKey === null) { + return null; + } + $command = static::getDb()->createCommand(); + $result = $command->get(static::index(), static::type(), $primaryKey, $options); + if ($result['exists']) { + $model = static::instantiate($result); + static::populateRecord($model, $result); + $model->afterFind(); + + return $model; + } + + return null; + } + + /** + * Gets a list of records by its primary keys. + * + * @param array $primaryKeys an array of primaryKey values + * @param array $options options given in this parameter are passed to elasticsearch + * as request URI parameters. + * + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) + * for more details on these options. + * @return static|null The record instance or null if it was not found. + */ + + public static function mget($primaryKeys, $options = []) + { + if (empty($primaryKeys)) { + return []; + } + $command = static::getDb()->createCommand(); + $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); + $models = []; + foreach ($result['docs'] as $doc) { + if ($doc['exists']) { + $model = static::instantiate($doc); + static::populateRecord($model, $doc); + $model->afterFind(); + $models[] = $model; + } + } + + return $models; + } + + // TODO add more like this feature http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-more-like-this.html + + // TODO add percolate functionality http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-percolate.html + + /** + * Creates an [[ActiveQuery]] instance. + * + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also + * by [[hasOne()]] and [[hasMany()]] to create a relational query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new ActiveQuery($config); + } + + // TODO implement copy and move as pk change is not possible + + /** + * @return float returns the score of this record when it was retrieved via a [[find()]] query. + */ + public function getScore() + { + return $this->_score; + } + + /** + * Sets the primary key + * @param mixed $value + * @throws \yii\base\InvalidCallException when record is not new + */ + public function setPrimaryKey($value) + { + $pk = static::primaryKey()[0]; + if ($this->getIsNewRecord() || $pk != '_id') { + $this->$pk = $value; + } else { + throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); + } + } + + /** + * @inheritdoc + */ + public function getPrimaryKey($asArray = false) + { + $pk = static::primaryKey()[0]; + if ($asArray) { + return [$pk => $this->$pk]; + } else { + return $this->$pk; + } + } + + /** + * @inheritdoc + */ + public function getOldPrimaryKey($asArray = false) + { + $pk = static::primaryKey()[0]; + if ($this->getIsNewRecord()) { + $id = null; + } elseif ($pk == '_id') { + $id = $this->_id; + } else { + $id = $this->getOldAttribute($pk); + } + if ($asArray) { + return [$pk => $id]; + } else { + return $id; + } + } + + /** + * This method defines the attribute that uniquely identifies a record. + * + * The primaryKey for elasticsearch documents is the `_id` field by default. This field is not part of the + * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]]. + * + * You may overide this method to define the primary key name when you have defined + * [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) + * for the `_id` field so that it is part of the `_source` and thus part of the [[attributes()|attributes]]. + * + * Note that elasticsearch only supports _one_ attribute to be the primary key. However to match the signature + * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a + * single string. + * + * @return string[] array of primary key attributes. Only the first element of the array will be used. + */ + public static function primaryKey() + { + return ['_id']; + } + + /** + * Returns the list of all attribute names of the model. + * + * This method must be overridden by child classes to define available attributes. + * + * Attributes are names of fields of the corresponding elasticsearch document. + * The primaryKey for elasticsearch documents is the `_id` field by default which is not part of the attributes. + * You may define [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) + * for the `_id` field so that it is part of the `_source` fields and thus becomes part of the attributes. + * + * @return string[] list of attribute names. + */ + public function attributes() + { + throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); + } + + /** + * @return string the name of the index this record is stored in. + */ + public static function index() + { + return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); + } + + /** + * @return string the name of the type of this record. + */ + public static function type() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); + } + + /** + * @inheritdoc + */ + public static function populateRecord($record, $row) + { + parent::populateRecord($record, $row['_source']); + $pk = static::primaryKey()[0]; + if ($pk === '_id') { + $record->_id = $row['_id']; + } + $record->_score = isset($row['_score']) ? $row['_score'] : null; + $record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available... + } + + /** + * Creates an active record instance. + * + * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. + * It is not meant to be used for creating new records directly. + * + * You may override this method if the instance being created + * depends on the row data to be populated into the record. + * For example, by creating a record based on the value of a column, + * you may implement the so-called single-table inheritance mapping. + * @param array $row row data to be populated into the record. + * This array consists of the following keys: + * - `_source`: refers to the attributes of the record. + * - `_type`: the type this record is stored in. + * - `_index`: the index this record is stored in. + * @return static the newly created active record + */ + public static function instantiate($row) + { + return new static; + } + + /** + * Inserts a document into the associated index using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. + * + * If the [[primaryKey|primary key]] is not set (null) during insertion, + * it will be populated with a + * [randomly generated value](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) + * after insertion. + * + * For example, to insert a customer record: + * + * ~~~ + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes will be saved. + * @param array $options options given in this parameter are passed to elasticsearch + * as request URI parameters. These are among others: + * + * - `routing` define shard placement of this record. + * - `parent` by giving the primaryKey of another record this defines a parent-child relation + * - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. + * + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) + * for more details on these options. + * + * By default the `op_type` is set to `create`. + * @return boolean whether the attributes are valid and the record is inserted successfully. + */ + public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + if ($this->beforeSave(true)) { + $values = $this->getDirtyAttributes($attributes); + + $response = static::getDb()->createCommand()->insert( + static::index(), + static::type(), + $values, + $this->getPrimaryKey(), + $options + ); + + if (!isset($response['ok'])) { + return false; + } + $pk = static::primaryKey()[0]; + $this->$pk = $response['_id']; + if ($pk != '_id') { + $values[$pk] = $response['_id']; + } + $this->_version = $response['_version']; + $this->_score = null; + $this->setOldAttributes($values); + $this->afterSave(true); + + return true; + } + + return false; + } + + /** + * Updates all records whos primary keys are given. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], [2, 3, 4]); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = []) + { + $pkName = static::primaryKey()[0]; + if (count($condition) == 1 && isset($condition[$pkName])) { + $primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]]; + } else { + $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id + } + if (empty($primaryKeys)) { + return 0; + } + $bulk = ''; + foreach ($primaryKeys as $pk) { + $action = Json::encode([ + "update" => [ + "_id" => $pk, + "_type" => static::type(), + "_index" => static::index(), + ], + ]); + $data = Json::encode([ + "doc" => $attributes + ]); + $bulk .= $action . "\n" . $data . "\n"; + } + + // TODO do this via command + $url = [static::index(), static::type(), '_bulk']; + $response = static::getDb()->post($url, [], $bulk); + $n = 0; + $errors = []; + foreach ($response['items'] as $item) { + if (isset($item['update']['error'])) { + $errors[] = $item['update']; + } elseif ($item['update']['ok']) { + $n++; + } + } + if (!empty($errors)) { + throw new Exception(__METHOD__ . ' failed updating records.', $errors); + } + + return $n; + } + + /** + * Updates all matching records using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = []) + { + $pkName = static::primaryKey()[0]; + if (count($condition) == 1 && isset($condition[$pkName])) { + $primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]]; + } else { + $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id + } + if (empty($primaryKeys) || empty($counters)) { + return 0; + } + $bulk = ''; + foreach ($primaryKeys as $pk) { + $action = Json::encode([ + "update" => [ + "_id" => $pk, + "_type" => static::type(), + "_index" => static::index(), + ], + ]); + $script = ''; + foreach ($counters as $counter => $value) { + $script .= "ctx._source.$counter += $counter;\n"; + } + $data = Json::encode([ + "script" => $script, + "params" => $counters + ]); + $bulk .= $action . "\n" . $data . "\n"; + } + + // TODO do this via command + $url = [static::index(), static::type(), '_bulk']; + $response = static::getDb()->post($url, [], $bulk); + $n = 0; + $errors = []; + foreach ($response['items'] as $item) { + if (isset($item['update']['error'])) { + $errors[] = $item['update']; + } elseif ($item['update']['ok']) { + $n++; + } + } + if (!empty($errors)) { + throw new Exception(__METHOD__ . ' failed updating records counters.', $errors); + } + + return $n; + } + + /** + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll('status = 3'); + * ~~~ + * + * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = []) + { + $pkName = static::primaryKey()[0]; + if (count($condition) == 1 && isset($condition[$pkName])) { + $primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]]; + } else { + $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id + } + if (empty($primaryKeys)) { + return 0; + } + $bulk = ''; + foreach ($primaryKeys as $pk) { + $bulk .= Json::encode([ + "delete" => [ + "_id" => $pk, + "_type" => static::type(), + "_index" => static::index(), + ], + ]) . "\n"; + } + + // TODO do this via command + $url = [static::index(), static::type(), '_bulk']; + $response = static::getDb()->post($url, [], $bulk); + $n = 0; + $errors = []; + foreach ($response['items'] as $item) { + if (isset($item['delete']['error'])) { + $errors[] = $item['delete']; + } elseif ($item['delete']['found'] && $item['delete']['ok']) { + $n++; + } + } + if (!empty($errors)) { + throw new Exception(__METHOD__ . ' failed deleting records.', $errors); + } + + return $n; + } } diff --git a/extensions/elasticsearch/Command.php b/extensions/elasticsearch/Command.php index 69c374665c8..db2b62b8b22 100644 --- a/extensions/elasticsearch/Command.php +++ b/extensions/elasticsearch/Command.php @@ -21,383 +21,388 @@ */ class Command extends Component { - /** - * @var Connection - */ - public $db; - /** - * @var string|array the indexes to execute the query on. Defaults to null meaning all indexes - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index - */ - public $index; - /** - * @var string|array the types to execute the query on. Defaults to null meaning all types - */ - public $type; - /** - * @var array list of arrays or json strings that become parts of a query - */ - public $queryParts; - - public $options = []; - - /** - * @param array $options - * @return mixed - */ - public function search($options = []) - { - $query = $this->queryParts; - if (empty($query)) { - $query = '{}'; - } - if (is_array($query)) { - $query = Json::encode($query); - } - $url = [ - $this->index !== null ? $this->index : '_all', - $this->type !== null ? $this->type : '_all', - '_search' - ]; - return $this->db->get($url, array_merge($this->options, $options), $query); - } - - /** - * Inserts a document into an index - * @param string $index - * @param string $type - * @param string|array $data json string or array of data to store - * @param null $id the documents id. If not specified Id will be automatically chosen - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html - */ - public function insert($index, $type, $data, $id = null, $options = []) - { - $body = is_array($data) ? Json::encode($data) : $data; - - if ($id !== null) { - return $this->db->put([$index, $type, $id], $options, $body); - } else { - return $this->db->post([$index, $type], $options, $body); - } - } - - /** - * gets a document from the index - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html - */ - public function get($index, $type, $id, $options = []) - { - return $this->db->get([$index, $type, $id], $options, null); - } - - /** - * gets multiple documents from the index - * - * TODO allow specifying type and index + fields - * @param $index - * @param $type - * @param $ids - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html - */ - public function mget($index, $type, $ids, $options = []) - { - $body = Json::encode(['ids' => array_values($ids)]); - return $this->db->get([$index, $type, '_mget'], $options, $body); - } - - /** - * gets a documents _source from the index (>=v0.90.1) - * @param $index - * @param $type - * @param $id - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source - */ - public function getSource($index, $type, $id) - { - return $this->db->get([$index, $type, $id]); - } - - /** - * gets a document from the index - * @param $index - * @param $type - * @param $id - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html - */ - public function exists($index, $type, $id) - { - return $this->db->head([$index, $type, $id]); - } - - /** - * deletes a document from the index - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html - */ - public function delete($index, $type, $id, $options = []) - { - return $this->db->delete([$index, $type, $id], $options); - } - - /** - * updates a document - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html - */ + /** + * @var Connection + */ + public $db; + /** + * @var string|array the indexes to execute the query on. Defaults to null meaning all indexes + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index + */ + public $index; + /** + * @var string|array the types to execute the query on. Defaults to null meaning all types + */ + public $type; + /** + * @var array list of arrays or json strings that become parts of a query + */ + public $queryParts; + + public $options = []; + + /** + * @param array $options + * @return mixed + */ + public function search($options = []) + { + $query = $this->queryParts; + if (empty($query)) { + $query = '{}'; + } + if (is_array($query)) { + $query = Json::encode($query); + } + $url = [ + $this->index !== null ? $this->index : '_all', + $this->type !== null ? $this->type : '_all', + '_search' + ]; + + return $this->db->get($url, array_merge($this->options, $options), $query); + } + + /** + * Inserts a document into an index + * @param string $index + * @param string $type + * @param string|array $data json string or array of data to store + * @param null $id the documents id. If not specified Id will be automatically chosen + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html + */ + public function insert($index, $type, $data, $id = null, $options = []) + { + $body = is_array($data) ? Json::encode($data) : $data; + + if ($id !== null) { + return $this->db->put([$index, $type, $id], $options, $body); + } else { + return $this->db->post([$index, $type], $options, $body); + } + } + + /** + * gets a document from the index + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html + */ + public function get($index, $type, $id, $options = []) + { + return $this->db->get([$index, $type, $id], $options, null); + } + + /** + * gets multiple documents from the index + * + * TODO allow specifying type and index + fields + * @param $index + * @param $type + * @param $ids + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html + */ + public function mget($index, $type, $ids, $options = []) + { + $body = Json::encode(['ids' => array_values($ids)]); + + return $this->db->get([$index, $type, '_mget'], $options, $body); + } + + /** + * gets a documents _source from the index (>=v0.90.1) + * @param $index + * @param $type + * @param $id + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source + */ + public function getSource($index, $type, $id) + { + return $this->db->get([$index, $type, $id]); + } + + /** + * gets a document from the index + * @param $index + * @param $type + * @param $id + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html + */ + public function exists($index, $type, $id) + { + return $this->db->head([$index, $type, $id]); + } + + /** + * deletes a document from the index + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html + */ + public function delete($index, $type, $id, $options = []) + { + return $this->db->delete([$index, $type, $id], $options); + } + + /** + * updates a document + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html + */ // public function update($index, $type, $id, $data, $options = []) // { // // TODO implement //// return $this->db->delete([$index, $type, $id], $options); // } - // TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html - - /** - * creates an index - * @param $index - * @param array $configuration - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html - */ - public function createIndex($index, $configuration = null) - { - $body = $configuration !== null ? Json::encode($configuration) : null; - return $this->db->put([$index], $body); - } - - /** - * deletes an index - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html - */ - public function deleteIndex($index) - { - return $this->db->delete([$index]); - } - - /** - * deletes all indexes - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html - */ - public function deleteAllIndexes() - { - return $this->db->delete(['_all']); - } - - /** - * checks whether an index exists - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html - */ - public function indexExists($index) - { - return $this->db->head([$index]); - } - - /** - * @param $index - * @param $type - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html - */ - public function typeExists($index, $type) - { - return $this->db->head([$index, $type]); - } - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html - - /** - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html - */ - public function openIndex($index) - { - return $this->db->post([$index, '_open']); - } - - /** - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html - */ - public function closeIndex($index) - { - return $this->db->post([$index, '_close']); - } - - /** - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html - */ - public function getIndexStatus($index = '_all') - { - return $this->db->get([$index, '_status']); - } - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html - // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html - - /** - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html - */ - public function clearIndexCache($index) - { - return $this->db->post([$index, '_cache', 'clear']); - } - - /** - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html - */ - public function flushIndex($index = '_all') - { - return $this->db->post([$index, '_flush']); - } - - /** - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html - */ - public function refreshIndex($index) - { - return $this->db->post([$index, '_refresh']); - } - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html - - /** - * @param $index - * @param $type - * @param $mapping - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html - */ - public function setMapping($index, $type, $mapping, $options = []) - { - $body = $mapping !== null ? (is_string($mapping) ? $mapping : Json::encode($mapping)) : null; - return $this->db->put([$index, $type, '_mapping'], $options, $body); - } - - /** - * @param string $index - * @param string $type - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html - */ - public function getMapping($index = '_all', $type = '_all') - { - return $this->db->get([$index, $type, '_mapping']); - } - - /** - * @param $index - * @param $type - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html - */ - public function deleteMapping($index, $type) - { - return $this->db->delete([$index, $type]); - } - - /** - * @param $index - * @param string $type - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html - */ - public function getFieldMapping($index, $type = '_all') - { - return $this->db->put([$index, $type, '_mapping']); - } - - /** - * @param $options - * @param $index - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html - */ + // TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html + + /** + * creates an index + * @param $index + * @param array $configuration + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html + */ + public function createIndex($index, $configuration = null) + { + $body = $configuration !== null ? Json::encode($configuration) : null; + + return $this->db->put([$index], $body); + } + + /** + * deletes an index + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html + */ + public function deleteIndex($index) + { + return $this->db->delete([$index]); + } + + /** + * deletes all indexes + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html + */ + public function deleteAllIndexes() + { + return $this->db->delete(['_all']); + } + + /** + * checks whether an index exists + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html + */ + public function indexExists($index) + { + return $this->db->head([$index]); + } + + /** + * @param $index + * @param $type + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html + */ + public function typeExists($index, $type) + { + return $this->db->head([$index, $type]); + } + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html + + /** + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html + */ + public function openIndex($index) + { + return $this->db->post([$index, '_open']); + } + + /** + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html + */ + public function closeIndex($index) + { + return $this->db->post([$index, '_close']); + } + + /** + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html + */ + public function getIndexStatus($index = '_all') + { + return $this->db->get([$index, '_status']); + } + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html + // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html + + /** + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html + */ + public function clearIndexCache($index) + { + return $this->db->post([$index, '_cache', 'clear']); + } + + /** + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html + */ + public function flushIndex($index = '_all') + { + return $this->db->post([$index, '_flush']); + } + + /** + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html + */ + public function refreshIndex($index) + { + return $this->db->post([$index, '_refresh']); + } + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html + + /** + * @param $index + * @param $type + * @param $mapping + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html + */ + public function setMapping($index, $type, $mapping, $options = []) + { + $body = $mapping !== null ? (is_string($mapping) ? $mapping : Json::encode($mapping)) : null; + + return $this->db->put([$index, $type, '_mapping'], $options, $body); + } + + /** + * @param string $index + * @param string $type + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html + */ + public function getMapping($index = '_all', $type = '_all') + { + return $this->db->get([$index, $type, '_mapping']); + } + + /** + * @param $index + * @param $type + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html + */ + public function deleteMapping($index, $type) + { + return $this->db->delete([$index, $type]); + } + + /** + * @param $index + * @param string $type + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html + */ + public function getFieldMapping($index, $type = '_all') + { + return $this->db->put([$index, $type, '_mapping']); + } + + /** + * @param $options + * @param $index + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html + */ // public function analyze($options, $index = null) // { // // TODO implement //// return $this->db->put([$index]); // } - /** - * @param $name - * @param $pattern - * @param $settings - * @param $mappings - * @param integer $order - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html - */ - public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) - { - $body = Json::encode([ - 'template' => $pattern, - 'order' => $order, - 'settings' => (object) $settings, - 'mappings' => (object) $mappings, - ]); - return $this->db->put(['_template', $name], $body); - - } - - /** - * @param $name - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html - */ - public function deleteTemplate($name) - { - return $this->db->delete(['_template', $name]); - - } - - /** - * @param $name - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html - */ - public function getTemplate($name) - { - return $this->db->get(['_template', $name]); - } + /** + * @param $name + * @param $pattern + * @param $settings + * @param $mappings + * @param integer $order + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html + */ + public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) + { + $body = Json::encode([ + 'template' => $pattern, + 'order' => $order, + 'settings' => (object) $settings, + 'mappings' => (object) $mappings, + ]); + + return $this->db->put(['_template', $name], $body); + + } + + /** + * @param $name + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html + */ + public function deleteTemplate($name) + { + return $this->db->delete(['_template', $name]); + + } + + /** + * @param $name + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html + */ + public function getTemplate($name) + { + return $this->db->get(['_template', $name]); + } } diff --git a/extensions/elasticsearch/Connection.php b/extensions/elasticsearch/Connection.php index 412351a3bf0..af62e88d50c 100644 --- a/extensions/elasticsearch/Connection.php +++ b/extensions/elasticsearch/Connection.php @@ -23,336 +23,345 @@ */ class Connection extends Component { - /** - * @event Event an event that is triggered after a DB connection is established - */ - const EVENT_AFTER_OPEN = 'afterOpen'; - - /** - * @var boolean whether to autodetect available cluster nodes on [[open()]] - */ - public $autodetectCluster = true; - /** - * @var array cluster nodes - * This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true. - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info - */ - public $nodes = [ - ['http_address' => 'inet[/127.0.0.1:9200]'], - ]; - /** - * @var array the active node. key of [[nodes]]. Will be randomly selected on [[open()]]. - */ - public $activeNode; - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth - public $auth = []; - /** - * @var float timeout to use for connecting to an elasticsearch node. - * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. - * If not set, no explicit timeout will be set for curl. - */ - public $connectionTimeout = null; - /** - * @var float timeout to use when reading the response from an elasticsearch node. - * This value will be used to configure the curl `CURLOPT_TIMEOUT` option. - * If not set, no explicit timeout will be set for curl. - */ - public $dataTimeout = null; - - - public function init() - { - foreach ($this->nodes as $node) { - if (!isset($node['http_address'])) { - throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.'); - } - } - } - - /** - * Closes the connection when this component is being serialized. - * @return array - */ - public function __sleep() - { - $this->close(); - return array_keys(get_object_vars($this)); - } - - /** - * Returns a value indicating whether the DB connection is established. - * @return boolean whether the DB connection is established - */ - public function getIsActive() - { - return $this->activeNode !== null; - } - - /** - * Establishes a DB connection. - * It does nothing if a DB connection has already been established. - * @throws Exception if connection fails - */ - public function open() - { - if ($this->activeNode !== null) { - return; - } - if (empty($this->nodes)) { - throw new InvalidConfigException('elasticsearch needs at least one node to operate.'); - } - if ($this->autodetectCluster) { - $node = reset($this->nodes); - $host = $node['http_address']; - if (strncmp($host, 'inet[/', 6) == 0) { - $host = substr($host, 6, -1); - } - $response = $this->httpRequest('GET', 'http://' . $host . '/_cluster/nodes'); - $this->nodes = $response['nodes']; - if (empty($this->nodes)) { - throw new Exception('cluster autodetection did not find any active node.'); - } - } - $this->selectActiveNode(); - Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes) - . ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__); - $this->initConnection(); - } - - /** - * select active node randomly - */ - protected function selectActiveNode() - { - $keys = array_keys($this->nodes); - $this->activeNode = $keys[rand(0, count($keys) - 1)]; - } - - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - Yii::trace('Closing connection to elasticsearch. Active node was: ' - . $this->nodes[$this->activeNode]['http_address'], __CLASS__); - $this->activeNode = null; - } - - /** - * Initializes the DB connection. - * This method is invoked right after the DB connection is established. - * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. - */ - protected function initConnection() - { - $this->trigger(self::EVENT_AFTER_OPEN); - } - - /** - * Returns the name of the DB driver for the current [[dsn]]. - * @return string name of the DB driver - */ - public function getDriverName() - { - return 'elasticsearch'; - } - - /** - * Creates a command for execution. - * @param array $config the configuration for the Command class - * @return Command the DB command - */ - public function createCommand($config = []) - { - $this->open(); - $config['db'] = $this; - $command = new Command($config); - return $command; - } - - public function getQueryBuilder() - { - return new QueryBuilder($this); - } - - public function get($url, $options = [], $body = null, $raw = false) - { - $this->open(); - return $this->httpRequest('GET', $this->createUrl($url, $options), $body, $raw); - } - - public function head($url, $options = [], $body = null) - { - $this->open(); - return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body); - } - - public function post($url, $options = [], $body = null, $raw = false) - { - $this->open(); - return $this->httpRequest('POST', $this->createUrl($url, $options), $body, $raw); - } - - public function put($url, $options = [], $body = null, $raw = false) - { - $this->open(); - return $this->httpRequest('PUT', $this->createUrl($url, $options), $body, $raw); - } - - public function delete($url, $options = [], $body = null, $raw = false) - { - $this->open(); - return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body, $raw); - } - - private function createUrl($path, $options = []) - { - if (!is_string($path)) { - $url = implode('/', array_map(function ($a) { - return urlencode(is_array($a) ? implode(',', $a) : $a); - }, $path)); - if (!empty($options)) { - $url .= '?' . http_build_query($options); - } - } else { - $url = $path; - if (!empty($options)) { - $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options); - } - } - return [$this->nodes[$this->activeNode]['http_address'], $url]; - } - - protected function httpRequest($method, $url, $requestBody = null, $raw = false) - { - $method = strtoupper($method); - - // response body and headers - $headers = []; - $body = ''; - - $options = [ - CURLOPT_USERAGENT => 'Yii Framework 2 ' . __CLASS__, - CURLOPT_RETURNTRANSFER => false, - CURLOPT_HEADER => false, - // http://www.php.net/manual/en/function.curl-setopt.php#82418 - CURLOPT_HTTPHEADER => ['Expect:'], - - CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) { - $body .= $data; - return mb_strlen($data, '8bit'); - }, - CURLOPT_HEADERFUNCTION => function($curl, $data) use (&$headers) { - foreach (explode("\r\n", $data) as $row) { - if (($pos = strpos($row, ':')) !== false) { - $headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1)); - } - } - return mb_strlen($data, '8bit'); - }, - CURLOPT_CUSTOMREQUEST => $method, - ]; - if ($this->connectionTimeout !== null) { - $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout; - } - if ($this->dataTimeout !== null) { - $options[CURLOPT_TIMEOUT] = $this->dataTimeout; - } - if ($requestBody !== null) { - $options[CURLOPT_POSTFIELDS] = $requestBody; - } - if ($method == 'HEAD') { - $options[CURLOPT_NOBODY] = true; - unset($options[CURLOPT_WRITEFUNCTION]); - } - - if (is_array($url)) { - list($host, $q) = $url; - if (strncmp($host, 'inet[', 5) == 0) { - $host = substr($host, 5, -1); - if (($pos = strpos($host, '/')) !== false) { - $host = substr($host, $pos + 1); - } - } - $profile = $method . ' ' . $q . '#' . $requestBody; - $url = 'http://' . $host . '/' . $q; - } else { - $profile = false; - } - - Yii::trace("Sending request to elasticsearch node: $url\n$requestBody", __METHOD__); - if ($profile !== false) { - Yii::beginProfile($profile, __METHOD__); - } - - $curl = curl_init($url); - curl_setopt_array($curl, $options); - if (curl_exec($curl) === false) { - throw new Exception('Elasticsearch request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseHeaders' => $headers, - 'responseBody' => $body, - ]); - } - - $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - - if ($profile !== false) { - Yii::endProfile($profile, __METHOD__); - } - - if ($responseCode >= 200 && $responseCode < 300) { - if ($method == 'HEAD') { - return true; - } else { - if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) { - throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseCode' => $responseCode, - 'responseHeaders' => $headers, - 'responseBody' => $body, - ]); - } - if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) { - return $raw ? $body : Json::decode($body); - } - throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseCode' => $responseCode, - 'responseHeaders' => $headers, - 'responseBody' => $body, - ]); - } - } elseif ($responseCode == 404) { - return false; - } else { - throw new Exception("Elasticsearch request failed with code $responseCode.", [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseCode' => $responseCode, - 'responseHeaders' => $headers, - 'responseBody' => $body, - ]); - } - } - - public function getNodeInfo() - { - return $this->get([]); - } - - public function getClusterState() - { - return $this->get(['_cluster', 'state']); - } + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; + + /** + * @var boolean whether to autodetect available cluster nodes on [[open()]] + */ + public $autodetectCluster = true; + /** + * @var array cluster nodes + * This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true. + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info + */ + public $nodes = [ + ['http_address' => 'inet[/127.0.0.1:9200]'], + ]; + /** + * @var array the active node. key of [[nodes]]. Will be randomly selected on [[open()]]. + */ + public $activeNode; + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth + public $auth = []; + /** + * @var float timeout to use for connecting to an elasticsearch node. + * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. + * If not set, no explicit timeout will be set for curl. + */ + public $connectionTimeout = null; + /** + * @var float timeout to use when reading the response from an elasticsearch node. + * This value will be used to configure the curl `CURLOPT_TIMEOUT` option. + * If not set, no explicit timeout will be set for curl. + */ + public $dataTimeout = null; + + public function init() + { + foreach ($this->nodes as $node) { + if (!isset($node['http_address'])) { + throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.'); + } + } + } + + /** + * Closes the connection when this component is being serialized. + * @return array + */ + public function __sleep() + { + $this->close(); + + return array_keys(get_object_vars($this)); + } + + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return $this->activeNode !== null; + } + + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->activeNode !== null) { + return; + } + if (empty($this->nodes)) { + throw new InvalidConfigException('elasticsearch needs at least one node to operate.'); + } + if ($this->autodetectCluster) { + $node = reset($this->nodes); + $host = $node['http_address']; + if (strncmp($host, 'inet[/', 6) == 0) { + $host = substr($host, 6, -1); + } + $response = $this->httpRequest('GET', 'http://' . $host . '/_cluster/nodes'); + $this->nodes = $response['nodes']; + if (empty($this->nodes)) { + throw new Exception('cluster autodetection did not find any active node.'); + } + } + $this->selectActiveNode(); + Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes) + . ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__); + $this->initConnection(); + } + + /** + * select active node randomly + */ + protected function selectActiveNode() + { + $keys = array_keys($this->nodes); + $this->activeNode = $keys[rand(0, count($keys) - 1)]; + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + Yii::trace('Closing connection to elasticsearch. Active node was: ' + . $this->nodes[$this->activeNode]['http_address'], __CLASS__); + $this->activeNode = null; + } + + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } + + /** + * Returns the name of the DB driver for the current [[dsn]]. + * @return string name of the DB driver + */ + public function getDriverName() + { + return 'elasticsearch'; + } + + /** + * Creates a command for execution. + * @param array $config the configuration for the Command class + * @return Command the DB command + */ + public function createCommand($config = []) + { + $this->open(); + $config['db'] = $this; + $command = new Command($config); + + return $command; + } + + public function getQueryBuilder() + { + return new QueryBuilder($this); + } + + public function get($url, $options = [], $body = null, $raw = false) + { + $this->open(); + + return $this->httpRequest('GET', $this->createUrl($url, $options), $body, $raw); + } + + public function head($url, $options = [], $body = null) + { + $this->open(); + + return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body); + } + + public function post($url, $options = [], $body = null, $raw = false) + { + $this->open(); + + return $this->httpRequest('POST', $this->createUrl($url, $options), $body, $raw); + } + + public function put($url, $options = [], $body = null, $raw = false) + { + $this->open(); + + return $this->httpRequest('PUT', $this->createUrl($url, $options), $body, $raw); + } + + public function delete($url, $options = [], $body = null, $raw = false) + { + $this->open(); + + return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body, $raw); + } + + private function createUrl($path, $options = []) + { + if (!is_string($path)) { + $url = implode('/', array_map(function ($a) { + return urlencode(is_array($a) ? implode(',', $a) : $a); + }, $path)); + if (!empty($options)) { + $url .= '?' . http_build_query($options); + } + } else { + $url = $path; + if (!empty($options)) { + $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options); + } + } + + return [$this->nodes[$this->activeNode]['http_address'], $url]; + } + + protected function httpRequest($method, $url, $requestBody = null, $raw = false) + { + $method = strtoupper($method); + + // response body and headers + $headers = []; + $body = ''; + + $options = [ + CURLOPT_USERAGENT => 'Yii Framework 2 ' . __CLASS__, + CURLOPT_RETURNTRANSFER => false, + CURLOPT_HEADER => false, + // http://www.php.net/manual/en/function.curl-setopt.php#82418 + CURLOPT_HTTPHEADER => ['Expect:'], + + CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) { + $body .= $data; + + return mb_strlen($data, '8bit'); + }, + CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers) { + foreach (explode("\r\n", $data) as $row) { + if (($pos = strpos($row, ':')) !== false) { + $headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1)); + } + } + + return mb_strlen($data, '8bit'); + }, + CURLOPT_CUSTOMREQUEST => $method, + ]; + if ($this->connectionTimeout !== null) { + $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout; + } + if ($this->dataTimeout !== null) { + $options[CURLOPT_TIMEOUT] = $this->dataTimeout; + } + if ($requestBody !== null) { + $options[CURLOPT_POSTFIELDS] = $requestBody; + } + if ($method == 'HEAD') { + $options[CURLOPT_NOBODY] = true; + unset($options[CURLOPT_WRITEFUNCTION]); + } + + if (is_array($url)) { + list($host, $q) = $url; + if (strncmp($host, 'inet[', 5) == 0) { + $host = substr($host, 5, -1); + if (($pos = strpos($host, '/')) !== false) { + $host = substr($host, $pos + 1); + } + } + $profile = $method . ' ' . $q . '#' . $requestBody; + $url = 'http://' . $host . '/' . $q; + } else { + $profile = false; + } + + Yii::trace("Sending request to elasticsearch node: $url\n$requestBody", __METHOD__); + if ($profile !== false) { + Yii::beginProfile($profile, __METHOD__); + } + + $curl = curl_init($url); + curl_setopt_array($curl, $options); + if (curl_exec($curl) === false) { + throw new Exception('Elasticsearch request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [ + 'requestMethod' => $method, + 'requestUrl' => $url, + 'requestBody' => $requestBody, + 'responseHeaders' => $headers, + 'responseBody' => $body, + ]); + } + + $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if ($profile !== false) { + Yii::endProfile($profile, __METHOD__); + } + + if ($responseCode >= 200 && $responseCode < 300) { + if ($method == 'HEAD') { + return true; + } else { + if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) { + throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [ + 'requestMethod' => $method, + 'requestUrl' => $url, + 'requestBody' => $requestBody, + 'responseCode' => $responseCode, + 'responseHeaders' => $headers, + 'responseBody' => $body, + ]); + } + if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) { + return $raw ? $body : Json::decode($body); + } + throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [ + 'requestMethod' => $method, + 'requestUrl' => $url, + 'requestBody' => $requestBody, + 'responseCode' => $responseCode, + 'responseHeaders' => $headers, + 'responseBody' => $body, + ]); + } + } elseif ($responseCode == 404) { + return false; + } else { + throw new Exception("Elasticsearch request failed with code $responseCode.", [ + 'requestMethod' => $method, + 'requestUrl' => $url, + 'requestBody' => $requestBody, + 'responseCode' => $responseCode, + 'responseHeaders' => $headers, + 'responseBody' => $body, + ]); + } + } + + public function getNodeInfo() + { + return $this->get([]); + } + + public function getClusterState() + { + return $this->get(['_cluster', 'state']); + } } diff --git a/extensions/elasticsearch/DebugAction.php b/extensions/elasticsearch/DebugAction.php index 503a54f840a..59b178cba0c 100644 --- a/extensions/elasticsearch/DebugAction.php +++ b/extensions/elasticsearch/DebugAction.php @@ -23,61 +23,62 @@ */ class DebugAction extends Action { - /** - * @var string the connection id to use - */ - public $db; - /** + /** + * @var string the connection id to use + */ + public $db; + /** * @var Panel - */ - public $panel; + */ + public $panel; - public function run($logId, $tag) - { - $this->controller->loadData($tag); + public function run($logId, $tag) + { + $this->controller->loadData($tag); - $timings = $this->panel->calculateTimings(); - ArrayHelper::multisort($timings, 3, SORT_DESC); - if (!isset($timings[$logId])) { - throw new HttpException(404, 'Log message not found.'); - } - $message = $timings[$logId][1]; - if (($pos = mb_strpos($message, "#")) !== false) { - $url = mb_substr($message, 0, $pos); - $body = mb_substr($message, $pos + 1); - } else { - $url = $message; - $body = null; - } - $method = mb_substr($url, 0, $pos = mb_strpos($url, ' ')); - $url = mb_substr($url, $pos + 1); + $timings = $this->panel->calculateTimings(); + ArrayHelper::multisort($timings, 3, SORT_DESC); + if (!isset($timings[$logId])) { + throw new HttpException(404, 'Log message not found.'); + } + $message = $timings[$logId][1]; + if (($pos = mb_strpos($message, "#")) !== false) { + $url = mb_substr($message, 0, $pos); + $body = mb_substr($message, $pos + 1); + } else { + $url = $message; + $body = null; + } + $method = mb_substr($url, 0, $pos = mb_strpos($url, ' ')); + $url = mb_substr($url, $pos + 1); - $options = ['pretty' => true]; + $options = ['pretty' => true]; - /** @var Connection $db */ - $db = \Yii::$app->getComponent($this->db); - $time = microtime(true); - switch($method) { - case 'GET': $result = $db->get($url, $options, $body, true); break; - case 'POST': $result = $db->post($url, $options, $body, true); break; - case 'PUT': $result = $db->put($url, $options, $body, true); break; - case 'DELETE': $result = $db->delete($url, $options, $body, true); break; - case 'HEAD': $result = $db->head($url, $options, $body); break; - default: - throw new NotSupportedException("Request method '$method' is not supported by elasticsearch."); - } - $time = microtime(true) - $time; + /** @var Connection $db */ + $db = \Yii::$app->getComponent($this->db); + $time = microtime(true); + switch ($method) { + case 'GET': $result = $db->get($url, $options, $body, true); break; + case 'POST': $result = $db->post($url, $options, $body, true); break; + case 'PUT': $result = $db->put($url, $options, $body, true); break; + case 'DELETE': $result = $db->delete($url, $options, $body, true); break; + case 'HEAD': $result = $db->head($url, $options, $body); break; + default: + throw new NotSupportedException("Request method '$method' is not supported by elasticsearch."); + } + $time = microtime(true) - $time; - if ($result === true) { - $result = 'success'; - } elseif ($result === false) { - $result = 'no success'; - } + if ($result === true) { + $result = 'success'; + } elseif ($result === false) { + $result = 'no success'; + } - Yii::$app->response->format = Response::FORMAT_JSON; - return [ - 'time' => sprintf('%.1f ms', $time * 1000), - 'result' => $result, - ]; - } + Yii::$app->response->format = Response::FORMAT_JSON; + + return [ + 'time' => sprintf('%.1f ms', $time * 1000), + 'result' => $result, + ]; + } } diff --git a/extensions/elasticsearch/DebugPanel.php b/extensions/elasticsearch/DebugPanel.php index b7ac2f3bcda..fcd3acaf418 100644 --- a/extensions/elasticsearch/DebugPanel.php +++ b/extensions/elasticsearch/DebugPanel.php @@ -22,113 +22,115 @@ */ class DebugPanel extends Panel { - public $db = 'elasticsearch'; - - public function init() - { - $this->actions['elasticsearch-query'] = [ - 'class' => 'yii\\elasticsearch\\DebugAction', - 'panel' => $this, - 'db' => $this->db, - ]; - } - - public function getName() - { - return 'Elasticsearch'; - } - - public function getSummary() - { - $timings = $this->calculateTimings(); - $queryCount = count($timings); - $queryTime = 0; - foreach ($timings as $timing) { - $queryTime += $timing[3]; - } - $queryTime = number_format($queryTime * 1000) . ' ms'; - $url = $this->getUrl(); - $output = <<actions['elasticsearch-query'] = [ + 'class' => 'yii\\elasticsearch\\DebugAction', + 'panel' => $this, + 'db' => $this->db, + ]; + } + + public function getName() + { + return 'Elasticsearch'; + } + + public function getSummary() + { + $timings = $this->calculateTimings(); + $queryCount = count($timings); + $queryTime = 0; + foreach ($timings as $timing) { + $queryTime += $timing[3]; + } + $queryTime = number_format($queryTime * 1000) . ' ms'; + $url = $this->getUrl(); + $output = << - - ES $queryCount $queryTime - + + ES $queryCount $queryTime + EOD; - return $queryCount > 0 ? $output : ''; - } - - public function getDetail() - { - $timings = $this->calculateTimings(); - ArrayHelper::multisort($timings, 3, SORT_DESC); - $rows = []; - $i = 0; - foreach ($timings as $logId => $timing) { - $duration = sprintf('%.1f ms', $timing[3] * 1000); - $message = $timing[1]; - $traces = $timing[4]; - if (($pos = mb_strpos($message, "#")) !== false) { - $url = mb_substr($message, 0, $pos); - $body = mb_substr($message, $pos + 1); - } else { - $url = $message; - $body = null; - } - $traceString = ''; - if (!empty($traces)) { - $traceString .= Html::ul($traces, [ - 'class' => 'trace', - 'item' => function ($trace) { - return "
  • {$trace['file']}({$trace['line']})
  • "; - }, - ]); - } - $ajaxUrl = Url::to(['elasticsearch-query', 'logId' => $logId, 'tag' => $this->tag]); - \Yii::$app->view->registerJs(<<Error: ' + errorThrown + ' - ' + textStatus + '
    ' + jqXHR.responseText); - }, - dataType: "json" - }); - - return false; + + return $queryCount > 0 ? $output : ''; + } + + public function getDetail() + { + $timings = $this->calculateTimings(); + ArrayHelper::multisort($timings, 3, SORT_DESC); + $rows = []; + $i = 0; + foreach ($timings as $logId => $timing) { + $duration = sprintf('%.1f ms', $timing[3] * 1000); + $message = $timing[1]; + $traces = $timing[4]; + if (($pos = mb_strpos($message, "#")) !== false) { + $url = mb_substr($message, 0, $pos); + $body = mb_substr($message, $pos + 1); + } else { + $url = $message; + $body = null; + } + $traceString = ''; + if (!empty($traces)) { + $traceString .= Html::ul($traces, [ + 'class' => 'trace', + 'item' => function ($trace) { + return "
  • {$trace['file']}({$trace['line']})
  • "; + }, + ]); + } + $ajaxUrl = Url::to(['elasticsearch-query', 'logId' => $logId, 'tag' => $this->tag]); + \Yii::$app->view->registerJs(<<Error: ' + errorThrown + ' - ' + textStatus + '
    ' + jqXHR.responseText); + }, + dataType: "json" + }); + + return false; }); JS , View::POS_READY); - $runLink = Html::a('run query', '#', ['id' => "elastic-link-$i"]) . '
    '; - $rows[] = << "elastic-link-$i"]) . '
    '; + $rows[] = << - $duration -
    $url

    $body

    $traceString
    - $runLink + $duration +
    $url

    $body

    $traceString
    + $runLink HTML; - $i++; - } - $rows = implode("\n", $rows); - return <<Elasticsearch Queries - - - + + + @@ -136,43 +138,45 @@ public function getDetail()
    TimeUrl / QueryRun Query on nodeTimeUrl / QueryRun Query on node
    HTML; - } - - private $_timings; - - public function calculateTimings() - { - if ($this->_timings !== null) { - return $this->_timings; - } - $messages = $this->data['messages']; - $timings = []; - $stack = []; - foreach ($messages as $i => $log) { - list($token, $level, $category, $timestamp) = $log; - $log[5] = $i; - if ($level == Logger::LEVEL_PROFILE_BEGIN) { - $stack[] = $log; - } elseif ($level == Logger::LEVEL_PROFILE_END) { - if (($last = array_pop($stack)) !== null && $last[0] === $token) { - $timings[$last[5]] = [count($stack), $token, $last[3], $timestamp - $last[3], $last[4]]; - } - } - } - - $now = microtime(true); - while (($last = array_pop($stack)) !== null) { - $delta = $now - $last[3]; - $timings[$last[5]] = [count($stack), $last[0], $last[2], $delta, $last[4]]; - } - ksort($timings); - return $this->_timings = $timings; - } - - public function save() - { - $target = $this->module->logTarget; - $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\elasticsearch\Connection::httpRequest']); - return ['messages' => $messages]; - } + } + + private $_timings; + + public function calculateTimings() + { + if ($this->_timings !== null) { + return $this->_timings; + } + $messages = $this->data['messages']; + $timings = []; + $stack = []; + foreach ($messages as $i => $log) { + list($token, $level, $category, $timestamp) = $log; + $log[5] = $i; + if ($level == Logger::LEVEL_PROFILE_BEGIN) { + $stack[] = $log; + } elseif ($level == Logger::LEVEL_PROFILE_END) { + if (($last = array_pop($stack)) !== null && $last[0] === $token) { + $timings[$last[5]] = [count($stack), $token, $last[3], $timestamp - $last[3], $last[4]]; + } + } + } + + $now = microtime(true); + while (($last = array_pop($stack)) !== null) { + $delta = $now - $last[3]; + $timings[$last[5]] = [count($stack), $last[0], $last[2], $delta, $last[4]]; + } + ksort($timings); + + return $this->_timings = $timings; + } + + public function save() + { + $target = $this->module->logTarget; + $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\elasticsearch\Connection::httpRequest']); + + return ['messages' => $messages]; + } } diff --git a/extensions/elasticsearch/Exception.php b/extensions/elasticsearch/Exception.php index 2510072abcb..dd4c1628e69 100644 --- a/extensions/elasticsearch/Exception.php +++ b/extensions/elasticsearch/Exception.php @@ -15,11 +15,11 @@ */ class Exception extends \yii\db\Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Elasticsearch Database Exception'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Elasticsearch Database Exception'; + } } diff --git a/extensions/elasticsearch/Query.php b/extensions/elasticsearch/Query.php index f392d161f68..63a2c887846 100644 --- a/extensions/elasticsearch/Query.php +++ b/extensions/elasticsearch/Query.php @@ -54,451 +54,463 @@ */ class Query extends Component implements QueryInterface { - use QueryTrait; - - /** - * @var array the fields being retrieved from the documents. For example, `['id', 'name']`. - * If not set, it means retrieving all fields. An empty array will result in no fields being - * retrieved. This means that only the primaryKey of a record will be available in the result. - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields - * @see fields() - */ - public $fields; - /** - * @var string|array The index to retrieve data from. This can be a string representing a single index - * or a an array of multiple indexes. If this is not set, indexes are being queried. - * @see from() - */ - public $index; - /** - * @var string|array The type to retrieve data from. This can be a string representing a single type - * or a an array of multiple types. If this is not set, all types are being queried. - * @see from() - */ - public $type; - /** - * @var integer A search timeout, bounding the search request to be executed within the specified time value - * and bail with the hits accumulated up to that point when expired. Defaults to no timeout. - * @see timeout() - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 - */ - public $timeout; - /** - * @var array|string The query part of this search query. This is an array or json string that follows the format of - * the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). - */ - public $query; - /** - * @var array|string The filter part of this search query. This is an array or json string that follows the format of - * the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). - */ - public $filter; - - public $facets = []; - - public function init() - { - parent::init(); - // setting the default limit according to elasticsearch defaults - // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 - if ($this->limit === null) { - $this->limit = 10; - } - } - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($db === null) { - $db = Yii::$app->getComponent('elasticsearch'); - } - - $commandConfig = $db->getQueryBuilder()->build($this); - return $db->createCommand($commandConfig); - } - - /** - * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $result = $this->createCommand($db)->search(); - if (empty($result['hits']['hits'])) { - return []; - } - $rows = $result['hits']['hits']; - if ($this->indexBy === null && $this->fields === null) { - return $rows; - } - $models = []; - foreach ($rows as $key => $row) { - if ($this->fields !== null) { - $row['_source'] = isset($row['fields']) ? $row['fields'] : []; - unset($row['fields']); - } - if ($this->indexBy !== null) { - if (is_string($this->indexBy)) { - $key = $row['_source'][$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - } - $models[$key] = $row; - } - return $models; - } - - /** - * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - public function one($db = null) - { - $result = $this->createCommand($db)->search(['size' => 1]); - if (empty($result['hits']['hits'])) { - return false; - } - $record = reset($result['hits']['hits']); - if ($this->fields !== null) { - $record['_source'] = isset($record['fields']) ? $record['fields'] : []; - unset($record['fields']); - } - return $record; - } - - /** - * Executes the query and returns the complete search result including e.g. hits, facets, totalCount. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @param array $options The options given with this query. Possible options are: - * - [routing](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-routing) - * - [search_type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html) - * @return array the query results. - */ - public function search($db = null, $options = []) - { - $result = $this->createCommand($db)->search($options); - if (!empty($result['hits']['hits']) && ($this->indexBy === null || $this->fields === null)) { - $rows = []; - foreach ($result['hits']['hits'] as $key => $row) { - if ($this->fields !== null) { - $row['_source'] = isset($row['fields']) ? $row['fields'] : []; - unset($row['fields']); - } - if ($this->indexBy !== null) { - if (is_string($this->indexBy)) { - $key = $row['_source'][$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - } - $rows[$key] = $row; - } - $result['hits']['hits'] = $rows; - } - return $result; - } - - // TODO add query stats http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#stats-groups - - // TODO add scroll/scan http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html#scan - - /** - * Executes the query and deletes all matching documents. - * - * This will not run facet queries. - * - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function delete($db = null) - { - // TODO implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html - throw new NotSupportedException('Delete by query is not implemented yet.'); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the specified field in the first document of the query results. - * @param string $field name of the attribute to select - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return string the value of the specified attribute in the first record of the query result. - * Null is returned if the query result is empty or the field does not exist. - */ - public function scalar($field, $db = null) - { - $record = self::one($db); // TODO limit fields to the one required - if ($record !== false && isset($record['_source'][$field])) { - return $record['_source'][$field]; - } else { - return null; - } - } - - /** - * Executes the query and returns the first column of the result. - * @param string $field the field to query over - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($field, $db = null) - { - $command = $this->createCommand($db); - $command->queryParts['fields'] = [$field]; - $result = $command->search(); - if (empty($result['hits']['hits'])) { - return []; - } - $column = []; - foreach ($result['hits']['hits'] as $row) { - $column[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; - } - return $column; - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. This parameter is ignored by this implementation. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null) - { - // TODO consider sending to _count api instead of _search for performance - // only when no facety are registerted. - // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html - - $options = []; - $options['search_type'] = 'count'; - return $this->createCommand($db)->search($options)['hits']['total']; - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - return self::one($db) !== false; - } - - /** - * Adds a facet search to this query. - * @param string $name the name of this facet - * @param string $type the facet type. e.g. `terms`, `range`, `histogram`... - * @param string|array $options the configuration options for this facet. Can be an array or a json string. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html - */ - public function addFacet($name, $type, $options) - { - $this->facets[$name] = [$type => $options]; - return $this; - } - - /** - * The `terms facet` allow to specify field facets that return the N most frequent terms. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html - */ - public function addTermFacet($name, $options) - { - return $this->addFacet($name, 'terms', $options); - } - - /** - * Range facet allows to specify a set of ranges and get both the number of docs (count) that fall - * within each range, and aggregated data either based on the field, or using another field. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html - */ - public function addRangeFacet($name, $options) - { - return $this->addFacet($name, 'range', $options); - } - - /** - * The histogram facet works with numeric data by building a histogram across intervals of the field values. - * Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per - * interval/bucket (count and total). - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html - */ - public function addHistogramFacet($name, $options) - { - return $this->addFacet($name, 'histogram', $options); - } - - /** - * A specific histogram facet that can work with date field types enhancing it over the regular histogram facet. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html - */ - public function addDateHistogramFacet($name, $options) - { - return $this->addFacet($name, 'date_histogram', $options); - } - - /** - * A filter facet (not to be confused with a facet filter) allows you to return a count of the hits matching the filter. - * The filter itself can be expressed using the Query DSL. - * @param string $name the name of this facet - * @param string $filter the query in Query DSL - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-filter-facet.html - */ - public function addFilterFacet($name, $filter) - { - return $this->addFacet($name, 'filter', $filter); - } - - /** - * A facet query allows to return a count of the hits matching the facet query. - * The query itself can be expressed using the Query DSL. - * @param string $name the name of this facet - * @param string $query the query in Query DSL - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html - */ - public function addQueryFacet($name, $query) - { - return $this->addFacet($name, 'query', $query); - } - - /** - * Statistical facet allows to compute statistical data on a numeric fields. The statistical data include count, - * total, sum of squares, mean (average), minimum, maximum, variance, and standard deviation. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-statistical-facet.html - */ - public function addStatisticalFacet($name, $options) - { - return $this->addFacet($name, 'statistical', $options); - } - - /** - * The `terms_stats` facet combines both the terms and statistical allowing to compute stats computed on a field, - * per term value driven by another field. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-stats-facet.html - */ - public function addTermsStatsFacet($name, $options) - { - return $this->addFacet($name, 'terms_stats', $options); - } - - /** - * The `geo_distance` facet is a facet providing information for ranges of distances from a provided `geo_point` - * including count of the number of hits that fall within each range, and aggregation information (like `total`). - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-geo-distance-facet.html - */ - public function addGeoDistanceFacet($name, $options) - { - return $this->addFacet($name, 'geo_distance', $options); - } - - // TODO add suggesters http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html - - // TODO add validate query http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-validate.html - - // TODO support multi query via static method http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html - - /** - * Sets the querypart of this search query. - * @param string $query - * @return static the query object itself - */ - public function query($query) - { - $this->query = $query; - return $this; - } - - /** - * Sets the filter part of this search query. - * @param string $filter - * @return static the query object itself - */ - public function filter($filter) - { - $this->filter = $filter; - return $this; - } - - /** - * Sets the index and type to retrieve documents from. - * @param string|array $index The index to retrieve data from. This can be a string representing a single index - * or a an array of multiple indexes. If this is `null` it means that all indexes are being queried. - * @param string|array $type The type to retrieve data from. This can be a string representing a single type - * or a an array of multiple types. If this is `null` it means that all types are being queried. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type - */ - public function from($index, $type = null) - { - $this->index = $index; - $this->type = $type; - return $this; - } - - /** - * Sets the fields to retrieve from the documents. - * @param array $fields the fields to be selected. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html - */ - public function fields($fields) - { - if (is_array($fields) || $fields === null) { - $this->fields = $fields; - } else { - $this->fields = func_get_args(); - } - return $this; - } - - /** - * Sets the search timeout. - * @param integer $timeout A search timeout, bounding the search request to be executed within the specified time value - * and bail with the hits accumulated up to that point when expired. Defaults to no timeout. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 - */ - public function timeout($timeout) - { - $this->timeout = $timeout; - return $this; - } + use QueryTrait; + + /** + * @var array the fields being retrieved from the documents. For example, `['id', 'name']`. + * If not set, it means retrieving all fields. An empty array will result in no fields being + * retrieved. This means that only the primaryKey of a record will be available in the result. + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields + * @see fields() + */ + public $fields; + /** + * @var string|array The index to retrieve data from. This can be a string representing a single index + * or a an array of multiple indexes. If this is not set, indexes are being queried. + * @see from() + */ + public $index; + /** + * @var string|array The type to retrieve data from. This can be a string representing a single type + * or a an array of multiple types. If this is not set, all types are being queried. + * @see from() + */ + public $type; + /** + * @var integer A search timeout, bounding the search request to be executed within the specified time value + * and bail with the hits accumulated up to that point when expired. Defaults to no timeout. + * @see timeout() + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 + */ + public $timeout; + /** + * @var array|string The query part of this search query. This is an array or json string that follows the format of + * the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). + */ + public $query; + /** + * @var array|string The filter part of this search query. This is an array or json string that follows the format of + * the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). + */ + public $filter; + + public $facets = []; + + public function init() + { + parent::init(); + // setting the default limit according to elasticsearch defaults + // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 + if ($this->limit === null) { + $this->limit = 10; + } + } + + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + if ($db === null) { + $db = Yii::$app->getComponent('elasticsearch'); + } + + $commandConfig = $db->getQueryBuilder()->build($this); + + return $db->createCommand($commandConfig); + } + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $result = $this->createCommand($db)->search(); + if (empty($result['hits']['hits'])) { + return []; + } + $rows = $result['hits']['hits']; + if ($this->indexBy === null && $this->fields === null) { + return $rows; + } + $models = []; + foreach ($rows as $key => $row) { + if ($this->fields !== null) { + $row['_source'] = isset($row['fields']) ? $row['fields'] : []; + unset($row['fields']); + } + if ($this->indexBy !== null) { + if (is_string($this->indexBy)) { + $key = $row['_source'][$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + } + $models[$key] = $row; + } + + return $models; + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null) + { + $result = $this->createCommand($db)->search(['size' => 1]); + if (empty($result['hits']['hits'])) { + return false; + } + $record = reset($result['hits']['hits']); + if ($this->fields !== null) { + $record['_source'] = isset($record['fields']) ? $record['fields'] : []; + unset($record['fields']); + } + + return $record; + } + + /** + * Executes the query and returns the complete search result including e.g. hits, facets, totalCount. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @param array $options The options given with this query. Possible options are: + * - [routing](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-routing) + * - [search_type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html) + * @return array the query results. + */ + public function search($db = null, $options = []) + { + $result = $this->createCommand($db)->search($options); + if (!empty($result['hits']['hits']) && ($this->indexBy === null || $this->fields === null)) { + $rows = []; + foreach ($result['hits']['hits'] as $key => $row) { + if ($this->fields !== null) { + $row['_source'] = isset($row['fields']) ? $row['fields'] : []; + unset($row['fields']); + } + if ($this->indexBy !== null) { + if (is_string($this->indexBy)) { + $key = $row['_source'][$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + } + $rows[$key] = $row; + } + $result['hits']['hits'] = $rows; + } + + return $result; + } + + // TODO add query stats http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#stats-groups + + // TODO add scroll/scan http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html#scan + + /** + * Executes the query and deletes all matching documents. + * + * This will not run facet queries. + * + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function delete($db = null) + { + // TODO implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + throw new NotSupportedException('Delete by query is not implemented yet.'); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the specified field in the first document of the query results. + * @param string $field name of the attribute to select + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return string the value of the specified attribute in the first record of the query result. + * Null is returned if the query result is empty or the field does not exist. + */ + public function scalar($field, $db = null) + { + $record = self::one($db); // TODO limit fields to the one required + if ($record !== false && isset($record['_source'][$field])) { + return $record['_source'][$field]; + } else { + return null; + } + } + + /** + * Executes the query and returns the first column of the result. + * @param string $field the field to query over + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($field, $db = null) + { + $command = $this->createCommand($db); + $command->queryParts['fields'] = [$field]; + $result = $command->search(); + if (empty($result['hits']['hits'])) { + return []; + } + $column = []; + foreach ($result['hits']['hits'] as $row) { + $column[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; + } + + return $column; + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. This parameter is ignored by this implementation. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null) + { + // TODO consider sending to _count api instead of _search for performance + // only when no facety are registerted. + // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html + + $options = []; + $options['search_type'] = 'count'; + + return $this->createCommand($db)->search($options)['hits']['total']; + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + return self::one($db) !== false; + } + + /** + * Adds a facet search to this query. + * @param string $name the name of this facet + * @param string $type the facet type. e.g. `terms`, `range`, `histogram`... + * @param string|array $options the configuration options for this facet. Can be an array or a json string. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html + */ + public function addFacet($name, $type, $options) + { + $this->facets[$name] = [$type => $options]; + + return $this; + } + + /** + * The `terms facet` allow to specify field facets that return the N most frequent terms. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html + */ + public function addTermFacet($name, $options) + { + return $this->addFacet($name, 'terms', $options); + } + + /** + * Range facet allows to specify a set of ranges and get both the number of docs (count) that fall + * within each range, and aggregated data either based on the field, or using another field. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html + */ + public function addRangeFacet($name, $options) + { + return $this->addFacet($name, 'range', $options); + } + + /** + * The histogram facet works with numeric data by building a histogram across intervals of the field values. + * Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per + * interval/bucket (count and total). + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html + */ + public function addHistogramFacet($name, $options) + { + return $this->addFacet($name, 'histogram', $options); + } + + /** + * A specific histogram facet that can work with date field types enhancing it over the regular histogram facet. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html + */ + public function addDateHistogramFacet($name, $options) + { + return $this->addFacet($name, 'date_histogram', $options); + } + + /** + * A filter facet (not to be confused with a facet filter) allows you to return a count of the hits matching the filter. + * The filter itself can be expressed using the Query DSL. + * @param string $name the name of this facet + * @param string $filter the query in Query DSL + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-filter-facet.html + */ + public function addFilterFacet($name, $filter) + { + return $this->addFacet($name, 'filter', $filter); + } + + /** + * A facet query allows to return a count of the hits matching the facet query. + * The query itself can be expressed using the Query DSL. + * @param string $name the name of this facet + * @param string $query the query in Query DSL + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html + */ + public function addQueryFacet($name, $query) + { + return $this->addFacet($name, 'query', $query); + } + + /** + * Statistical facet allows to compute statistical data on a numeric fields. The statistical data include count, + * total, sum of squares, mean (average), minimum, maximum, variance, and standard deviation. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-statistical-facet.html + */ + public function addStatisticalFacet($name, $options) + { + return $this->addFacet($name, 'statistical', $options); + } + + /** + * The `terms_stats` facet combines both the terms and statistical allowing to compute stats computed on a field, + * per term value driven by another field. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-stats-facet.html + */ + public function addTermsStatsFacet($name, $options) + { + return $this->addFacet($name, 'terms_stats', $options); + } + + /** + * The `geo_distance` facet is a facet providing information for ranges of distances from a provided `geo_point` + * including count of the number of hits that fall within each range, and aggregation information (like `total`). + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-geo-distance-facet.html + */ + public function addGeoDistanceFacet($name, $options) + { + return $this->addFacet($name, 'geo_distance', $options); + } + + // TODO add suggesters http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html + + // TODO add validate query http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-validate.html + + // TODO support multi query via static method http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html + + /** + * Sets the querypart of this search query. + * @param string $query + * @return static the query object itself + */ + public function query($query) + { + $this->query = $query; + + return $this; + } + + /** + * Sets the filter part of this search query. + * @param string $filter + * @return static the query object itself + */ + public function filter($filter) + { + $this->filter = $filter; + + return $this; + } + + /** + * Sets the index and type to retrieve documents from. + * @param string|array $index The index to retrieve data from. This can be a string representing a single index + * or a an array of multiple indexes. If this is `null` it means that all indexes are being queried. + * @param string|array $type The type to retrieve data from. This can be a string representing a single type + * or a an array of multiple types. If this is `null` it means that all types are being queried. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type + */ + public function from($index, $type = null) + { + $this->index = $index; + $this->type = $type; + + return $this; + } + + /** + * Sets the fields to retrieve from the documents. + * @param array $fields the fields to be selected. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html + */ + public function fields($fields) + { + if (is_array($fields) || $fields === null) { + $this->fields = $fields; + } else { + $this->fields = func_get_args(); + } + + return $this; + } + + /** + * Sets the search timeout. + * @param integer $timeout A search timeout, bounding the search request to be executed within the specified time value + * and bail with the hits accumulated up to that point when expired. Defaults to no timeout. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 + */ + public function timeout($timeout) + { + $this->timeout = $timeout; + + return $this; + } } diff --git a/extensions/elasticsearch/QueryBuilder.php b/extensions/elasticsearch/QueryBuilder.php index 0b6a38b06ef..30607ed1ef3 100644 --- a/extensions/elasticsearch/QueryBuilder.php +++ b/extensions/elasticsearch/QueryBuilder.php @@ -19,295 +19,302 @@ */ class QueryBuilder extends \yii\base\Object { - /** - * @var Connection the database connection. - */ - public $db; - - /** - * Constructor. - * @param Connection $connection the database connection. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($connection, $config = []) - { - $this->db = $connection; - parent::__construct($config); - } - - /** - * Generates query from a [[Query]] object. - * @param Query $query the [[Query]] object from which the query will be generated - * @return array the generated SQL statement (the first array element) and the corresponding - * parameters to be bound to the SQL statement (the second array element). - */ - public function build($query) - { - $parts = []; - - if ($query->fields !== null) { - $parts['fields'] = (array) $query->fields; - } - if ($query->limit !== null && $query->limit >= 0) { - $parts['size'] = $query->limit; - } - if ($query->offset > 0) { - $parts['from'] = (int) $query->offset; - } - - if (empty($query->query)) { - $parts['query'] = ["match_all" => (object)[]]; - } else { - $parts['query'] = $query->query; - } - - $whereFilter = $this->buildCondition($query->where); - if (is_string($query->filter)) { - if (empty($whereFilter)) { - $parts['filter'] = $query->filter; - } else { - $parts['filter'] = '{"and": [' . $query->filter . ', ' . Json::encode($whereFilter) . ']}'; - } - } elseif ($query->filter !== null) { - if (empty($whereFilter)) { - $parts['filter'] = $query->filter; - } else { - $parts['filter'] = ['and' => [$query->filter, $whereFilter]]; - } - } elseif (!empty($whereFilter)) { - $parts['filter'] = $whereFilter; - } - - $sort = $this->buildOrderBy($query->orderBy); - if (!empty($sort)) { - $parts['sort'] = $sort; - } - - if (!empty($query->facets)) { - $parts['facets'] = $query->facets; - } - - $options = []; - if ($query->timeout !== null) { - $options['timeout'] = $query->timeout; - } - - return [ - 'queryParts' => $parts, - 'index' => $query->index, - 'type' => $query->type, - 'options' => $options, - ]; - } - - /** - * adds order by condition to the query - */ - public function buildOrderBy($columns) - { - if (empty($columns)) { - return []; - } - $orders = []; - foreach ($columns as $name => $direction) { - if (is_string($direction)) { - $column = $direction; - $direction = SORT_ASC; - } else { - $column = $name; - } - if ($column == '_id') { - $column = '_uid'; - } - - // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ - if (is_array($direction)) { - $orders[] = [$column => $direction]; - } else { - $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; - } - } - return $orders; - } - - /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws \yii\db\Exception if the condition is in bad format - */ - public function buildCondition($condition) - { - static $builders = [ - 'not' => 'buildNotCondition', - 'and' => 'buildAndCondition', - 'or' => 'buildAndCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', - ]; - - if (empty($condition)) { - return []; - } - if (!is_array($condition)) { - throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtolower($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition); - } else { - throw new InvalidParamException('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition); - } - } - - private function buildHashCondition($condition) - { - $parts = []; - foreach ($condition as $attribute => $value) { - if ($attribute == '_id') { - if ($value == null) { // there is no null pk - $parts[] = ['script' => ['script' => '0==1']]; - } else { - $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; - } - } else { - if (is_array($value)) { // IN condition - $parts[] = ['in' => [$attribute => $value]]; - } else { - if ($value === null) { - $parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; - } else { - $parts[] = ['term' => [$attribute => $value]]; - } - } - } - } - return count($parts) === 1 ? $parts[0] : ['and' => $parts]; - } - - private function buildNotCondition($operator, $operands, &$params) - { - if (count($operands) != 1) { - throw new InvalidParamException("Operator '$operator' requires exactly one operand."); - } - - $operand = reset($operands); - if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); - } - return [$operator => $operand]; - } - - private function buildAndCondition($operator, $operands) - { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if (!empty($operand)) { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return [$operator => $parts]; - } else { - return []; - } - } - - private function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new InvalidParamException("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - if ($column == '_id') { - throw new NotSupportedException('Between condition is not supported for the _id field.'); - } - $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; - if ($operator == 'not between') { - $filter = ['not' => $filter]; - } - return $filter; - } - - private function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'in' ? ['script' => ['script' => '0==1']] : []; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); - } elseif (is_array($column)) { - $column = reset($column); - } - $canBeNull = false; - foreach ($values as $i => $value) { - if (is_array($value)) { - $values[$i] = $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $canBeNull = true; - unset($values[$i]); - } - } - if ($column == '_id') { - if (empty($values) && $canBeNull) { // there is no null pk - $filter = ['script' => ['script' => '0==1']]; - } else { - $filter = ['ids' => ['values' => array_values($values)]]; - if ($canBeNull) { - $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; - } - } - } else { - if (empty($values) && $canBeNull) { - $filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; - } else { - $filter = ['in' => [$column => array_values($values)]]; - if ($canBeNull) { - $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; - } - } - } - if ($operator == 'not in') { - $filter = ['not' => $filter]; - } - return $filter; - } - - protected function buildCompositeInCondition($operator, $columns, $values) - { - throw new NotSupportedException('composite in is not supported by elasticsearch.'); - } - - private function buildLikeCondition($operator, $operands) - { - throw new NotSupportedException('like conditions are not supported by elasticsearch.'); - } + /** + * @var Connection the database connection. + */ + public $db; + + /** + * Constructor. + * @param Connection $connection the database connection. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($connection, $config = []) + { + $this->db = $connection; + parent::__construct($config); + } + + /** + * Generates query from a [[Query]] object. + * @param Query $query the [[Query]] object from which the query will be generated + * @return array the generated SQL statement (the first array element) and the corresponding + * parameters to be bound to the SQL statement (the second array element). + */ + public function build($query) + { + $parts = []; + + if ($query->fields !== null) { + $parts['fields'] = (array) $query->fields; + } + if ($query->limit !== null && $query->limit >= 0) { + $parts['size'] = $query->limit; + } + if ($query->offset > 0) { + $parts['from'] = (int) $query->offset; + } + + if (empty($query->query)) { + $parts['query'] = ["match_all" => (object) []]; + } else { + $parts['query'] = $query->query; + } + + $whereFilter = $this->buildCondition($query->where); + if (is_string($query->filter)) { + if (empty($whereFilter)) { + $parts['filter'] = $query->filter; + } else { + $parts['filter'] = '{"and": [' . $query->filter . ', ' . Json::encode($whereFilter) . ']}'; + } + } elseif ($query->filter !== null) { + if (empty($whereFilter)) { + $parts['filter'] = $query->filter; + } else { + $parts['filter'] = ['and' => [$query->filter, $whereFilter]]; + } + } elseif (!empty($whereFilter)) { + $parts['filter'] = $whereFilter; + } + + $sort = $this->buildOrderBy($query->orderBy); + if (!empty($sort)) { + $parts['sort'] = $sort; + } + + if (!empty($query->facets)) { + $parts['facets'] = $query->facets; + } + + $options = []; + if ($query->timeout !== null) { + $options['timeout'] = $query->timeout; + } + + return [ + 'queryParts' => $parts, + 'index' => $query->index, + 'type' => $query->type, + 'options' => $options, + ]; + } + + /** + * adds order by condition to the query + */ + public function buildOrderBy($columns) + { + if (empty($columns)) { + return []; + } + $orders = []; + foreach ($columns as $name => $direction) { + if (is_string($direction)) { + $column = $direction; + $direction = SORT_ASC; + } else { + $column = $name; + } + if ($column == '_id') { + $column = '_uid'; + } + + // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ + if (is_array($direction)) { + $orders[] = [$column => $direction]; + } else { + $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; + } + } + + return $orders; + } + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition($condition) + { + static $builders = [ + 'not' => 'buildNotCondition', + 'and' => 'buildAndCondition', + 'or' => 'buildAndCondition', + 'between' => 'buildBetweenCondition', + 'not between' => 'buildBetweenCondition', + 'in' => 'buildInCondition', + 'not in' => 'buildInCondition', + 'like' => 'buildLikeCondition', + 'not like' => 'buildLikeCondition', + 'or like' => 'buildLikeCondition', + 'or not like' => 'buildLikeCondition', + ]; + + if (empty($condition)) { + return []; + } + if (!is_array($condition)) { + throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtolower($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + + return $this->$method($operator, $condition); + } else { + throw new InvalidParamException('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + + return $this->buildHashCondition($condition); + } + } + + private function buildHashCondition($condition) + { + $parts = []; + foreach ($condition as $attribute => $value) { + if ($attribute == '_id') { + if ($value == null) { // there is no null pk + $parts[] = ['script' => ['script' => '0==1']]; + } else { + $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; + } + } else { + if (is_array($value)) { // IN condition + $parts[] = ['in' => [$attribute => $value]]; + } else { + if ($value === null) { + $parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; + } else { + $parts[] = ['term' => [$attribute => $value]]; + } + } + } + } + + return count($parts) === 1 ? $parts[0] : ['and' => $parts]; + } + + private function buildNotCondition($operator, $operands, &$params) + { + if (count($operands) != 1) { + throw new InvalidParamException("Operator '$operator' requires exactly one operand."); + } + + $operand = reset($operands); + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + + return [$operator => $operand]; + } + + private function buildAndCondition($operator, $operands) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand); + } + if (!empty($operand)) { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return [$operator => $parts]; + } else { + return []; + } + } + + private function buildBetweenCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new InvalidParamException("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + if ($column == '_id') { + throw new NotSupportedException('Between condition is not supported for the _id field.'); + } + $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; + if ($operator == 'not between') { + $filter = ['not' => $filter]; + } + + return $filter; + } + + private function buildInCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1])) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array) $values; + + if (empty($values) || $column === []) { + return $operator === 'in' ? ['script' => ['script' => '0==1']] : []; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values); + } elseif (is_array($column)) { + $column = reset($column); + } + $canBeNull = false; + foreach ($values as $i => $value) { + if (is_array($value)) { + $values[$i] = $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $canBeNull = true; + unset($values[$i]); + } + } + if ($column == '_id') { + if (empty($values) && $canBeNull) { // there is no null pk + $filter = ['script' => ['script' => '0==1']]; + } else { + $filter = ['ids' => ['values' => array_values($values)]]; + if ($canBeNull) { + $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; + } + } + } else { + if (empty($values) && $canBeNull) { + $filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; + } else { + $filter = ['in' => [$column => array_values($values)]]; + if ($canBeNull) { + $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; + } + } + } + if ($operator == 'not in') { + $filter = ['not' => $filter]; + } + + return $filter; + } + + protected function buildCompositeInCondition($operator, $columns, $values) + { + throw new NotSupportedException('composite in is not supported by elasticsearch.'); + } + + private function buildLikeCondition($operator, $operands) + { + throw new NotSupportedException('like conditions are not supported by elasticsearch.'); + } } diff --git a/extensions/faker/FixtureController.php b/extensions/faker/FixtureController.php index befe2617012..b7134a6ea0a 100644 --- a/extensions/faker/FixtureController.php +++ b/extensions/faker/FixtureController.php @@ -138,233 +138,236 @@ */ class FixtureController extends \yii\console\controllers\FixtureController { - /** - * type of fixture generating - */ - const GENERATE_ALL = 'all'; - - /** - * @var string controller default action ID. - */ - public $defaultAction = 'generate'; - /** - * @var string Alias to the template path, where all tables templates are stored. - */ - public $templatePath = '@tests/unit/templates/fixtures'; - /** - * @var string Alias to the fixture data path, where data files should be written. - */ - public $fixtureDataPath = '@tests/unit/fixtures/data'; - /** - * @var string Language to use when generating fixtures data. - */ - public $language; - /** - * @var array Additional data providers that can be created by user and will be added to the Faker generator. - * More info in [Faker](https://github.com/fzaninotto/Faker.) library docs. - */ - public $providers = []; - /** - * @var \Faker\Generator Faker generator instance - */ - private $_generator; - - - /** - * Returns the names of the global options for this command. - * @return array the names of the global options for this command. - */ - public function options($id) - { - return array_merge(parent::options($id), [ - 'templatePath', 'language', 'fixtureDataPath' - ]); - } - - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $this->checkPaths(); - $this->addProviders(); - return true; - } else { - return false; - } - } - - /** - * Generates fixtures and fill them with Faker data. - * @param string $file filename for the table template. You can generate all fixtures for all tables - * by specifying keyword "all" as filename. - * @param integer $times how much fixtures do you want per table - */ - public function actionGenerate(array $file, $times = 2) - { - $templatePath = Yii::getAlias($this->templatePath); - $fixtureDataPath = Yii::getAlias($this->fixtureDataPath); - - if ($this->needToGenerateAll($file[0])) { - $files = FileHelper::findFiles($templatePath, ['only' => ['*.php']]); - } else { - $filesToSearch = []; - foreach ($file as $fileName) { - $filesToSearch[] = $fileName . '.php'; - } - $files = FileHelper::findFiles($templatePath, ['only' => $filesToSearch]); - } - - if (empty($files)) { - throw new Exception("No files were found by name: \"" . implode(', ', $file) . "\".\n" - . "Check that template with these name exists, under template path: \n\"{$templatePath}\"." - ); - } - - if (!$this->confirmGeneration($files)) { - return; - } - - foreach ($files as $templateFile) { - $fixtureFileName = basename($templateFile); - $template = $this->getTemplate($templateFile); - $fixtures = []; - - for ($i = 0; $i < $times; $i++) { - $fixtures[$i] = $this->generateFixture($template, $i); - } - - $content = $this->exportFixtures($fixtures); - FileHelper::createDirectory($fixtureDataPath); - file_put_contents($fixtureDataPath . '/'. $fixtureFileName, $content); - - $this->stdout("Fixture file was generated under: $fixtureDataPath\n", Console::FG_GREEN); - } - } - - /** - * Returns Faker generator instance. Getter for private property. - * @return \Faker\Generator - */ - public function getGenerator() - { - if (is_null($this->_generator)) { - //replacing - on _ because Faker support only en_US format and not intl - - $language = is_null($this->language) ? str_replace('-', '_', Yii::$app->language) : $this->language; - $this->_generator = \Faker\Factory::create($language); - } - - return $this->_generator; - } - - /** - * Check if the template path and migrations path exists and writable. - */ - public function checkPaths() - { - $path = Yii::getAlias($this->templatePath); - - if (!is_dir($path)) { - throw new Exception("The template path \"{$this->templatePath}\" not exist"); - } - } - - /** - * Adds users providers to the faker generator. - */ - public function addProviders() - { - foreach ($this->providers as $provider) { - $this->generator->addProvider(new $provider($this->generator)); - } - } - - /** - * Checks if needed to generate all fixtures. - * @param string $file - * @return bool - */ - public function needToGenerateAll($file) - { - return $file == self::GENERATE_ALL; - } - - /** - * Returns generator template for the given fixture name - * @param string $file template file - * @return array generator template - * @throws \yii\console\Exception if wrong file format - */ - public function getTemplate($file) - { - $template = require($file); - - if (!is_array($template)) { - throw new Exception("The template file \"$file\" has wrong format. It should return valid template array"); - } - - return $template; - } - - /** - * Returns exported to the string representation of given fixtures array. - * @param array $fixtures - * @return string exported fixtures format - */ - public function exportFixtures($fixtures) - { - $content = " $value) { - $content .= "\n\t\t'{$name}' => '{$value}',"; - } - - $content .= "\n\t],"; - - } - $content .= "\n];\n"; - return $content; - } - - /** - * Generates fixture from given template - * @param array $template fixture template - * @param integer $index current fixture index - * @return array fixture - */ - public function generateFixture($template, $index) - { - $fixture = []; - - foreach ($template as $attribute => $fakerProperty) { - if (!is_string($fakerProperty)) { - $fixture = call_user_func_array($fakerProperty, [$fixture, $this->generator, $index]); - } else { - $fixture[$attribute] = $this->generator->$fakerProperty; - } - } - - return $fixture; - } - - /** - * Prompts user with message if he confirm generation with given fixture templates files. - * @param array $files - * @return boolean - */ - public function confirmGeneration($files) - { - $this->stdout("Fixtures will be generated under the path: \n", Console::FG_YELLOW); - $this->stdout("\t" . Yii::getAlias($this->fixtureDataPath) . "\n\n", Console::FG_GREEN); - $this->stdout("Templates will be taken from path: \n", Console::FG_YELLOW); - $this->stdout("\t" . Yii::getAlias($this->templatePath) . "\n\n", Console::FG_GREEN); - - foreach ($files as $index => $fileName) { - $this->stdout(" " . ($index + 1) . ". " . basename($fileName) . "\n", Console::FG_GREEN); - } - return $this->confirm('Generate above fixtures?'); - } + /** + * type of fixture generating + */ + const GENERATE_ALL = 'all'; + + /** + * @var string controller default action ID. + */ + public $defaultAction = 'generate'; + /** + * @var string Alias to the template path, where all tables templates are stored. + */ + public $templatePath = '@tests/unit/templates/fixtures'; + /** + * @var string Alias to the fixture data path, where data files should be written. + */ + public $fixtureDataPath = '@tests/unit/fixtures/data'; + /** + * @var string Language to use when generating fixtures data. + */ + public $language; + /** + * @var array Additional data providers that can be created by user and will be added to the Faker generator. + * More info in [Faker](https://github.com/fzaninotto/Faker.) library docs. + */ + public $providers = []; + /** + * @var \Faker\Generator Faker generator instance + */ + private $_generator; + + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function options($id) + { + return array_merge(parent::options($id), [ + 'templatePath', 'language', 'fixtureDataPath' + ]); + } + + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $this->checkPaths(); + $this->addProviders(); + + return true; + } else { + return false; + } + } + + /** + * Generates fixtures and fill them with Faker data. + * @param string $file filename for the table template. You can generate all fixtures for all tables + * by specifying keyword "all" as filename. + * @param integer $times how much fixtures do you want per table + */ + public function actionGenerate(array $file, $times = 2) + { + $templatePath = Yii::getAlias($this->templatePath); + $fixtureDataPath = Yii::getAlias($this->fixtureDataPath); + + if ($this->needToGenerateAll($file[0])) { + $files = FileHelper::findFiles($templatePath, ['only' => ['*.php']]); + } else { + $filesToSearch = []; + foreach ($file as $fileName) { + $filesToSearch[] = $fileName . '.php'; + } + $files = FileHelper::findFiles($templatePath, ['only' => $filesToSearch]); + } + + if (empty($files)) { + throw new Exception("No files were found by name: \"" . implode(', ', $file) . "\".\n" + . "Check that template with these name exists, under template path: \n\"{$templatePath}\"." + ); + } + + if (!$this->confirmGeneration($files)) { + return; + } + + foreach ($files as $templateFile) { + $fixtureFileName = basename($templateFile); + $template = $this->getTemplate($templateFile); + $fixtures = []; + + for ($i = 0; $i < $times; $i++) { + $fixtures[$i] = $this->generateFixture($template, $i); + } + + $content = $this->exportFixtures($fixtures); + FileHelper::createDirectory($fixtureDataPath); + file_put_contents($fixtureDataPath . '/'. $fixtureFileName, $content); + + $this->stdout("Fixture file was generated under: $fixtureDataPath\n", Console::FG_GREEN); + } + } + + /** + * Returns Faker generator instance. Getter for private property. + * @return \Faker\Generator + */ + public function getGenerator() + { + if (is_null($this->_generator)) { + //replacing - on _ because Faker support only en_US format and not intl + + $language = is_null($this->language) ? str_replace('-', '_', Yii::$app->language) : $this->language; + $this->_generator = \Faker\Factory::create($language); + } + + return $this->_generator; + } + + /** + * Check if the template path and migrations path exists and writable. + */ + public function checkPaths() + { + $path = Yii::getAlias($this->templatePath); + + if (!is_dir($path)) { + throw new Exception("The template path \"{$this->templatePath}\" not exist"); + } + } + + /** + * Adds users providers to the faker generator. + */ + public function addProviders() + { + foreach ($this->providers as $provider) { + $this->generator->addProvider(new $provider($this->generator)); + } + } + + /** + * Checks if needed to generate all fixtures. + * @param string $file + * @return bool + */ + public function needToGenerateAll($file) + { + return $file == self::GENERATE_ALL; + } + + /** + * Returns generator template for the given fixture name + * @param string $file template file + * @return array generator template + * @throws \yii\console\Exception if wrong file format + */ + public function getTemplate($file) + { + $template = require($file); + + if (!is_array($template)) { + throw new Exception("The template file \"$file\" has wrong format. It should return valid template array"); + } + + return $template; + } + + /** + * Returns exported to the string representation of given fixtures array. + * @param array $fixtures + * @return string exported fixtures format + */ + public function exportFixtures($fixtures) + { + $content = " $value) { + $content .= "\n\t\t'{$name}' => '{$value}',"; + } + + $content .= "\n\t],"; + + } + $content .= "\n];\n"; + + return $content; + } + + /** + * Generates fixture from given template + * @param array $template fixture template + * @param integer $index current fixture index + * @return array fixture + */ + public function generateFixture($template, $index) + { + $fixture = []; + + foreach ($template as $attribute => $fakerProperty) { + if (!is_string($fakerProperty)) { + $fixture = call_user_func_array($fakerProperty, [$fixture, $this->generator, $index]); + } else { + $fixture[$attribute] = $this->generator->$fakerProperty; + } + } + + return $fixture; + } + + /** + * Prompts user with message if he confirm generation with given fixture templates files. + * @param array $files + * @return boolean + */ + public function confirmGeneration($files) + { + $this->stdout("Fixtures will be generated under the path: \n", Console::FG_YELLOW); + $this->stdout("\t" . Yii::getAlias($this->fixtureDataPath) . "\n\n", Console::FG_GREEN); + $this->stdout("Templates will be taken from path: \n", Console::FG_YELLOW); + $this->stdout("\t" . Yii::getAlias($this->templatePath) . "\n\n", Console::FG_GREEN); + + foreach ($files as $index => $fileName) { + $this->stdout(" " . ($index + 1) . ". " . basename($fileName) . "\n", Console::FG_GREEN); + } + + return $this->confirm('Generate above fixtures?'); + } } diff --git a/extensions/gii/CodeFile.php b/extensions/gii/CodeFile.php index 06210cb6280..11e4c2f9cc1 100644 --- a/extensions/gii/CodeFile.php +++ b/extensions/gii/CodeFile.php @@ -24,168 +24,170 @@ */ class CodeFile extends Object { - /** - * The code file is new. - */ - const OP_CREATE = 'create'; - /** - * The code file already exists, and the new one may need to overwrite it. - */ - const OP_OVERWRITE = 'overwrite'; - /** - * The new code file and the existing one are identical. - */ - const OP_SKIP = 'skip'; + /** + * The code file is new. + */ + const OP_CREATE = 'create'; + /** + * The code file already exists, and the new one may need to overwrite it. + */ + const OP_OVERWRITE = 'overwrite'; + /** + * The new code file and the existing one are identical. + */ + const OP_SKIP = 'skip'; - /** - * @var string an ID that uniquely identifies this code file. - */ - public $id; - /** - * @var string the file path that the new code should be saved to. - */ - public $path; - /** - * @var string the newly generated code content - */ - public $content; - /** - * @var string the operation to be performed. This can be [[OP_NEW]], [[OP_OVERWRITE]] or [[OP_SKIP]]. - */ - public $operation; + /** + * @var string an ID that uniquely identifies this code file. + */ + public $id; + /** + * @var string the file path that the new code should be saved to. + */ + public $path; + /** + * @var string the newly generated code content + */ + public $content; + /** + * @var string the operation to be performed. This can be [[OP_NEW]], [[OP_OVERWRITE]] or [[OP_SKIP]]. + */ + public $operation; - /** - * Constructor. - * @param string $path the file path that the new code should be saved to. - * @param string $content the newly generated code content. - */ - public function __construct($path, $content) - { - $this->path = strtr($path, ['/' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR]); - $this->content = $content; - $this->id = md5($this->path); - if (is_file($path)) { - $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE; - } else { - $this->operation = self::OP_CREATE; - } - } + /** + * Constructor. + * @param string $path the file path that the new code should be saved to. + * @param string $content the newly generated code content. + */ + public function __construct($path, $content) + { + $this->path = strtr($path, ['/' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR]); + $this->content = $content; + $this->id = md5($this->path); + if (is_file($path)) { + $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE; + } else { + $this->operation = self::OP_CREATE; + } + } - /** - * Saves the code into the file specified by [[path]]. - * @return string|boolean the error occurred while saving the code file, or true if no error. - */ - public function save() - { - $module = Yii::$app->controller->module; - if ($this->operation === self::OP_CREATE) { - $dir = dirname($this->path); - if (!is_dir($dir)) { - $mask = @umask(0); - $result = @mkdir($dir, $module->newDirMode, true); - @umask($mask); - if (!$result) { - return "Unable to create the directory '$dir'."; - } - } - } - if (@file_put_contents($this->path, $this->content) === false) { - return "Unable to write the file '{$this->path}'."; - } else { - $mask = @umask(0); - @chmod($this->path, $module->newFileMode); - @umask($mask); - } - return true; - } + /** + * Saves the code into the file specified by [[path]]. + * @return string|boolean the error occurred while saving the code file, or true if no error. + */ + public function save() + { + $module = Yii::$app->controller->module; + if ($this->operation === self::OP_CREATE) { + $dir = dirname($this->path); + if (!is_dir($dir)) { + $mask = @umask(0); + $result = @mkdir($dir, $module->newDirMode, true); + @umask($mask); + if (!$result) { + return "Unable to create the directory '$dir'."; + } + } + } + if (@file_put_contents($this->path, $this->content) === false) { + return "Unable to write the file '{$this->path}'."; + } else { + $mask = @umask(0); + @chmod($this->path, $module->newFileMode); + @umask($mask); + } - /** - * @return string the code file path relative to the application base path. - */ - public function getRelativePath() - { - if (strpos($this->path, Yii::$app->basePath) === 0) { - return substr($this->path, strlen(Yii::$app->basePath) + 1); - } else { - return $this->path; - } - } + return true; + } - /** - * @return string the code file extension (e.g. php, txt) - */ - public function getType() - { - if (($pos = strrpos($this->path, '.')) !== false) { - return substr($this->path, $pos + 1); - } else { - return 'unknown'; - } - } + /** + * @return string the code file path relative to the application base path. + */ + public function getRelativePath() + { + if (strpos($this->path, Yii::$app->basePath) === 0) { + return substr($this->path, strlen(Yii::$app->basePath) + 1); + } else { + return $this->path; + } + } - /** - * Returns preview or false if it cannot be rendered - * - * @return boolean|string - */ - public function preview() - { - if (($pos = strrpos($this->path, '.')) !== false) { - $type = substr($this->path, $pos + 1); - } else { - $type = 'unknown'; - } + /** + * @return string the code file extension (e.g. php, txt) + */ + public function getType() + { + if (($pos = strrpos($this->path, '.')) !== false) { + return substr($this->path, $pos + 1); + } else { + return 'unknown'; + } + } - if ($type === 'php') { - return highlight_string($this->content, true); - } elseif (!in_array($type, ['jpg', 'gif', 'png', 'exe'])) { - return nl2br(Html::encode($this->content)); - } else { - return false; - } - } + /** + * Returns preview or false if it cannot be rendered + * + * @return boolean|string + */ + public function preview() + { + if (($pos = strrpos($this->path, '.')) !== false) { + $type = substr($this->path, $pos + 1); + } else { + $type = 'unknown'; + } - /** - * Returns diff or false if it cannot be calculated - * - * @return boolean|string - */ - public function diff() - { - $type = strtolower($this->getType()); - if (in_array($type, ['jpg', 'gif', 'png', 'exe'])) { - return false; - } elseif ($this->operation === self::OP_OVERWRITE) { - return $this->renderDiff(file($this->path), $this->content); - } else { - return ''; - } - } + if ($type === 'php') { + return highlight_string($this->content, true); + } elseif (!in_array($type, ['jpg', 'gif', 'png', 'exe'])) { + return nl2br(Html::encode($this->content)); + } else { + return false; + } + } - /** - * Renders diff between two sets of lines - * - * @param mixed $lines1 - * @param mixed $lines2 - * @return string - */ - private function renderDiff($lines1, $lines2) - { - if (!is_array($lines1)) { - $lines1 = explode("\n", $lines1); - } - if (!is_array($lines2)) { - $lines2 = explode("\n", $lines2); - } - foreach ($lines1 as $i => $line) { - $lines1[$i] = rtrim($line, "\r\n"); - } - foreach ($lines2 as $i => $line) { - $lines2[$i] = rtrim($line, "\r\n"); - } + /** + * Returns diff or false if it cannot be calculated + * + * @return boolean|string + */ + public function diff() + { + $type = strtolower($this->getType()); + if (in_array($type, ['jpg', 'gif', 'png', 'exe'])) { + return false; + } elseif ($this->operation === self::OP_OVERWRITE) { + return $this->renderDiff(file($this->path), $this->content); + } else { + return ''; + } + } - $renderer = new DiffRendererHtmlInline(); - $diff = new \Diff($lines1, $lines2); - return $diff->render($renderer); - } + /** + * Renders diff between two sets of lines + * + * @param mixed $lines1 + * @param mixed $lines2 + * @return string + */ + private function renderDiff($lines1, $lines2) + { + if (!is_array($lines1)) { + $lines1 = explode("\n", $lines1); + } + if (!is_array($lines2)) { + $lines2 = explode("\n", $lines2); + } + foreach ($lines1 as $i => $line) { + $lines1[$i] = rtrim($line, "\r\n"); + } + foreach ($lines2 as $i => $line) { + $lines2[$i] = rtrim($line, "\r\n"); + } + + $renderer = new DiffRendererHtmlInline(); + $diff = new \Diff($lines1, $lines2); + + return $diff->render($renderer); + } } diff --git a/extensions/gii/Generator.php b/extensions/gii/Generator.php index 9dc3c6b5f40..794a0843ff1 100644 --- a/extensions/gii/Generator.php +++ b/extensions/gii/Generator.php @@ -37,411 +37,415 @@ */ abstract class Generator extends Model { - /** - * @var array a list of available code templates. The array keys are the template names, - * and the array values are the corresponding template paths or path aliases. - */ - public $templates = []; - /** - * @var string the name of the code template that the user has selected. - * The value of this property is internally managed by this class. - */ - public $template; + /** + * @var array a list of available code templates. The array keys are the template names, + * and the array values are the corresponding template paths or path aliases. + */ + public $templates = []; + /** + * @var string the name of the code template that the user has selected. + * The value of this property is internally managed by this class. + */ + public $template; - /** - * @return string name of the code generator - */ - abstract public function getName(); - /** - * Generates the code based on the current user input and the specified code template files. - * This is the main method that child classes should implement. - * Please refer to [[\yii\gii\generators\controller\Generator::generate()]] as an example - * on how to implement this method. - * @return CodeFile[] a list of code files to be created. - */ - abstract public function generate(); + /** + * @return string name of the code generator + */ + abstract public function getName(); + /** + * Generates the code based on the current user input and the specified code template files. + * This is the main method that child classes should implement. + * Please refer to [[\yii\gii\generators\controller\Generator::generate()]] as an example + * on how to implement this method. + * @return CodeFile[] a list of code files to be created. + */ + abstract public function generate(); - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (!isset($this->templates['default'])) { - $this->templates['default'] = $this->defaultTemplate(); - } - foreach ($this->templates as $i => $template) { - $this->templates[$i] = Yii::getAlias($template); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!isset($this->templates['default'])) { + $this->templates['default'] = $this->defaultTemplate(); + } + foreach ($this->templates as $i => $template) { + $this->templates[$i] = Yii::getAlias($template); + } + } - /** - * Returns a list of code template files that are required. - * Derived classes usually should override this method if they require the existence of - * certain template files. - * @return array list of code template files that are required. They should be file paths - * relative to [[templatePath]]. - */ - public function requiredTemplates() - { - return []; - } + /** + * Returns a list of code template files that are required. + * Derived classes usually should override this method if they require the existence of + * certain template files. + * @return array list of code template files that are required. They should be file paths + * relative to [[templatePath]]. + */ + public function requiredTemplates() + { + return []; + } - /** - * Returns the list of sticky attributes. - * A sticky attribute will remember its value and will initialize the attribute with this value - * when the generator is restarted. - * @return array list of sticky attributes - */ - public function stickyAttributes() - { - return ['template']; - } + /** + * Returns the list of sticky attributes. + * A sticky attribute will remember its value and will initialize the attribute with this value + * when the generator is restarted. + * @return array list of sticky attributes + */ + public function stickyAttributes() + { + return ['template']; + } - /** - * Returns the list of hint messages. - * The array keys are the attribute names, and the array values are the corresponding hint messages. - * Hint messages will be displayed to end users when they are filling the form for the generator. - * @return array the list of hint messages - */ - public function hints() - { - return []; - } + /** + * Returns the list of hint messages. + * The array keys are the attribute names, and the array values are the corresponding hint messages. + * Hint messages will be displayed to end users when they are filling the form for the generator. + * @return array the list of hint messages + */ + public function hints() + { + return []; + } - /** - * Returns the list of auto complete values. - * The array keys are the attribute names, and the array values are the corresponding auto complete values. - * Auto complete values can also be callable typed in order one want to make postponed data generation. - * @return array the list of auto complete values - */ - public function autoCompleteData() - { - return []; - } + /** + * Returns the list of auto complete values. + * The array keys are the attribute names, and the array values are the corresponding auto complete values. + * Auto complete values can also be callable typed in order one want to make postponed data generation. + * @return array the list of auto complete values + */ + public function autoCompleteData() + { + return []; + } - /** - * Returns the message to be displayed when the newly generated code is saved successfully. - * Child classes may override this method to customize the message. - * @return string the message to be displayed when the newly generated code is saved successfully. - */ - public function successMessage() - { - return 'The code has been generated successfully.'; - } + /** + * Returns the message to be displayed when the newly generated code is saved successfully. + * Child classes may override this method to customize the message. + * @return string the message to be displayed when the newly generated code is saved successfully. + */ + public function successMessage() + { + return 'The code has been generated successfully.'; + } - /** - * Returns the view file for the input form of the generator. - * The default implementation will return the "form.php" file under the directory - * that contains the generator class file. - * @return string the view file for the input form of the generator. - */ - public function formView() - { - $class = new ReflectionClass($this); - return dirname($class->getFileName()) . '/form.php'; - } + /** + * Returns the view file for the input form of the generator. + * The default implementation will return the "form.php" file under the directory + * that contains the generator class file. + * @return string the view file for the input form of the generator. + */ + public function formView() + { + $class = new ReflectionClass($this); - /** - * Returns the root path to the default code template files. - * The default implementation will return the "templates" subdirectory of the - * directory containing the generator class file. - * @return string the root path to the default code template files. - */ - public function defaultTemplate() - { - $class = new ReflectionClass($this); - return dirname($class->getFileName()) . '/templates'; - } + return dirname($class->getFileName()) . '/form.php'; + } - /** - * @return string the detailed description of the generator. - */ - public function getDescription() - { - return ''; - } + /** + * Returns the root path to the default code template files. + * The default implementation will return the "templates" subdirectory of the + * directory containing the generator class file. + * @return string the root path to the default code template files. + */ + public function defaultTemplate() + { + $class = new ReflectionClass($this); - /** - * @inheritdoc - * - * Child classes should override this method like the following so that the parent - * rules are included: - * - * ~~~ - * return array_merge(parent::rules(), [ - * ...rules for the child class... - * ]); - * ~~~ - */ - public function rules() - { - return [ - [['template'], 'required', 'message' => 'A code template must be selected.'], - [['template'], 'validateTemplate'], - ]; - } + return dirname($class->getFileName()) . '/templates'; + } - /** - * Loads sticky attributes from an internal file and populates them into the generator. - * @internal - */ - public function loadStickyAttributes() - { - $stickyAttributes = $this->stickyAttributes(); - $path = $this->getStickyDataFile(); - if (is_file($path)) { - $result = json_decode(file_get_contents($path), true); - if (is_array($result)) { - foreach ($stickyAttributes as $name) { - if (isset($result[$name])) { - $this->$name = $result[$name]; - } - } - } - } - } + /** + * @return string the detailed description of the generator. + */ + public function getDescription() + { + return ''; + } - /** - * Saves sticky attributes into an internal file. - * @internal - */ - public function saveStickyAttributes() - { - $stickyAttributes = $this->stickyAttributes(); - $stickyAttributes[] = 'template'; - $values = []; - foreach ($stickyAttributes as $name) { - $values[$name] = $this->$name; - } - $path = $this->getStickyDataFile(); - @mkdir(dirname($path), 0755, true); - file_put_contents($path, json_encode($values)); - } + /** + * @inheritdoc + * + * Child classes should override this method like the following so that the parent + * rules are included: + * + * ~~~ + * return array_merge(parent::rules(), [ + * ...rules for the child class... + * ]); + * ~~~ + */ + public function rules() + { + return [ + [['template'], 'required', 'message' => 'A code template must be selected.'], + [['template'], 'validateTemplate'], + ]; + } - /** - * @return string the file path that stores the sticky attribute values. - * @internal - */ - public function getStickyDataFile() - { - return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-', get_class($this)) . '.json'; - } + /** + * Loads sticky attributes from an internal file and populates them into the generator. + * @internal + */ + public function loadStickyAttributes() + { + $stickyAttributes = $this->stickyAttributes(); + $path = $this->getStickyDataFile(); + if (is_file($path)) { + $result = json_decode(file_get_contents($path), true); + if (is_array($result)) { + foreach ($stickyAttributes as $name) { + if (isset($result[$name])) { + $this->$name = $result[$name]; + } + } + } + } + } - /** - * Saves the generated code into files. - * @param CodeFile[] $files the code files to be saved - * @param array $answers - * @param string $results this parameter receives a value from this method indicating the log messages - * generated while saving the code files. - * @return boolean whether there is any error while saving the code files. - */ - public function save($files, $answers, &$results) - { - $lines = ['Generating code using template "' . $this->getTemplatePath() . '"...']; - $hasError = false; - foreach ($files as $file) { - $relativePath = $file->getRelativePath(); - if (isset($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) { - $error = $file->save(); - if (is_string($error)) { - $hasError = true; - $lines[] = "generating $relativePath\n$error"; - } else { - $lines[] = $file->operation === CodeFile::OP_CREATE ? " generated $relativePath" : " overwrote $relativePath"; - } - } else { - $lines[] = " skipped $relativePath"; - } - } - $lines[] = "done!\n"; - $results = implode("\n", $lines); + /** + * Saves sticky attributes into an internal file. + * @internal + */ + public function saveStickyAttributes() + { + $stickyAttributes = $this->stickyAttributes(); + $stickyAttributes[] = 'template'; + $values = []; + foreach ($stickyAttributes as $name) { + $values[$name] = $this->$name; + } + $path = $this->getStickyDataFile(); + @mkdir(dirname($path), 0755, true); + file_put_contents($path, json_encode($values)); + } - return $hasError; - } + /** + * @return string the file path that stores the sticky attribute values. + * @internal + */ + public function getStickyDataFile() + { + return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-', get_class($this)) . '.json'; + } - /** - * @return string the root path of the template files that are currently being used. - * @throws InvalidConfigException if [[template]] is invalid - */ - public function getTemplatePath() - { - if (isset($this->templates[$this->template])) { - return $this->templates[$this->template]; - } else { - throw new InvalidConfigException("Unknown template: {$this->template}"); - } - } + /** + * Saves the generated code into files. + * @param CodeFile[] $files the code files to be saved + * @param array $answers + * @param string $results this parameter receives a value from this method indicating the log messages + * generated while saving the code files. + * @return boolean whether there is any error while saving the code files. + */ + public function save($files, $answers, &$results) + { + $lines = ['Generating code using template "' . $this->getTemplatePath() . '"...']; + $hasError = false; + foreach ($files as $file) { + $relativePath = $file->getRelativePath(); + if (isset($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) { + $error = $file->save(); + if (is_string($error)) { + $hasError = true; + $lines[] = "generating $relativePath\n$error"; + } else { + $lines[] = $file->operation === CodeFile::OP_CREATE ? " generated $relativePath" : " overwrote $relativePath"; + } + } else { + $lines[] = " skipped $relativePath"; + } + } + $lines[] = "done!\n"; + $results = implode("\n", $lines); - /** - * Generates code using the specified code template and parameters. - * Note that the code template will be used as a PHP file. - * @param string $template the code template file. This must be specified as a file path - * relative to [[templatePath]]. - * @param array $params list of parameters to be passed to the template file. - * @return string the generated code - */ - public function render($template, $params = []) - { - $view = new View; - $params['generator'] = $this; - return $view->renderFile($this->getTemplatePath() . '/' . $template, $params, $this); - } + return $hasError; + } - /** - * Validates the template selection. - * This method validates whether the user selects an existing template - * and the template contains all required template files as specified in [[requiredTemplates()]]. - */ - public function validateTemplate() - { - $templates = $this->templates; - if (!isset($templates[$this->template])) { - $this->addError('template', 'Invalid template selection.'); - } else { - $templatePath = $this->templates[$this->template]; - foreach ($this->requiredTemplates() as $template) { - if (!is_file($templatePath . '/' . $template)) { - $this->addError('template', "Unable to find the required code template file '$template'."); - } - } - } - } + /** + * @return string the root path of the template files that are currently being used. + * @throws InvalidConfigException if [[template]] is invalid + */ + public function getTemplatePath() + { + if (isset($this->templates[$this->template])) { + return $this->templates[$this->template]; + } else { + throw new InvalidConfigException("Unknown template: {$this->template}"); + } + } - /** - * An inline validator that checks if the attribute value refers to an existing class name. - * If the `extends` option is specified, it will also check if the class is a child class - * of the class represented by the `extends` option. - * @param string $attribute the attribute being validated - * @param array $params the validation options - */ - public function validateClass($attribute, $params) - { - $class = $this->$attribute; - try { - if (class_exists($class)) { - if (isset($params['extends'])) { - if (ltrim($class, '\\') !== ltrim($params['extends'], '\\') && !is_subclass_of($class, $params['extends'])) { - $this->addError($attribute, "'$class' must extend from {$params['extends']} or its child class."); - } - } - } else { - $this->addError($attribute, "Class '$class' does not exist or has syntax error."); - } - } catch (\Exception $e) { - $this->addError($attribute, "Class '$class' does not exist or has syntax error."); - } - } + /** + * Generates code using the specified code template and parameters. + * Note that the code template will be used as a PHP file. + * @param string $template the code template file. This must be specified as a file path + * relative to [[templatePath]]. + * @param array $params list of parameters to be passed to the template file. + * @return string the generated code + */ + public function render($template, $params = []) + { + $view = new View; + $params['generator'] = $this; - /** - * An inline validator that checks if the attribute value refers to a valid namespaced class name. - * The validator will check if the directory containing the new class file exist or not. - * @param string $attribute the attribute being validated - * @param array $params the validation options - */ - public function validateNewClass($attribute, $params) - { - $class = ltrim($this->$attribute, '\\'); - if (($pos = strrpos($class, '\\')) === false) { - $this->addError($attribute, "The class name must contain fully qualified namespace name."); - } else { - $ns = substr($class, 0, $pos); - $path = Yii::getAlias('@' . str_replace('\\', '/', $ns), false); - if ($path === false) { - $this->addError($attribute, "The class namespace is invalid: $ns"); - } elseif (!is_dir($path)) { - $this->addError($attribute, "Please make sure the directory containing this class exists: $path"); - } - } - } + return $view->renderFile($this->getTemplatePath() . '/' . $template, $params, $this); + } - /** - * @param string $value the attribute to be validated - * @return boolean whether the value is a reserved PHP keyword. - */ - public function isReservedKeyword($value) - { - static $keywords = [ - '__class__', - '__dir__', - '__file__', - '__function__', - '__line__', - '__method__', - '__namespace__', - '__trait__', - 'abstract', - 'and', - 'array', - 'as', - 'break', - 'case', - 'catch', - 'callable', - 'cfunction', - 'class', - 'clone', - 'const', - 'continue', - 'declare', - 'default', - 'die', - 'do', - 'echo', - 'else', - 'elseif', - 'empty', - 'enddeclare', - 'endfor', - 'endforeach', - 'endif', - 'endswitch', - 'endwhile', - 'eval', - 'exception', - 'exit', - 'extends', - 'final', - 'finally', - 'for', - 'foreach', - 'function', - 'global', - 'goto', - 'if', - 'implements', - 'include', - 'include_once', - 'instanceof', - 'insteadof', - 'interface', - 'isset', - 'list', - 'namespace', - 'new', - 'old_function', - 'or', - 'parent', - 'php_user_filter', - 'print', - 'private', - 'protected', - 'public', - 'require', - 'require_once', - 'return', - 'static', - 'switch', - 'this', - 'throw', - 'trait', - 'try', - 'unset', - 'use', - 'var', - 'while', - 'xor', - ]; - return in_array(strtolower($value), $keywords, true); - } + /** + * Validates the template selection. + * This method validates whether the user selects an existing template + * and the template contains all required template files as specified in [[requiredTemplates()]]. + */ + public function validateTemplate() + { + $templates = $this->templates; + if (!isset($templates[$this->template])) { + $this->addError('template', 'Invalid template selection.'); + } else { + $templatePath = $this->templates[$this->template]; + foreach ($this->requiredTemplates() as $template) { + if (!is_file($templatePath . '/' . $template)) { + $this->addError('template', "Unable to find the required code template file '$template'."); + } + } + } + } + + /** + * An inline validator that checks if the attribute value refers to an existing class name. + * If the `extends` option is specified, it will also check if the class is a child class + * of the class represented by the `extends` option. + * @param string $attribute the attribute being validated + * @param array $params the validation options + */ + public function validateClass($attribute, $params) + { + $class = $this->$attribute; + try { + if (class_exists($class)) { + if (isset($params['extends'])) { + if (ltrim($class, '\\') !== ltrim($params['extends'], '\\') && !is_subclass_of($class, $params['extends'])) { + $this->addError($attribute, "'$class' must extend from {$params['extends']} or its child class."); + } + } + } else { + $this->addError($attribute, "Class '$class' does not exist or has syntax error."); + } + } catch (\Exception $e) { + $this->addError($attribute, "Class '$class' does not exist or has syntax error."); + } + } + + /** + * An inline validator that checks if the attribute value refers to a valid namespaced class name. + * The validator will check if the directory containing the new class file exist or not. + * @param string $attribute the attribute being validated + * @param array $params the validation options + */ + public function validateNewClass($attribute, $params) + { + $class = ltrim($this->$attribute, '\\'); + if (($pos = strrpos($class, '\\')) === false) { + $this->addError($attribute, "The class name must contain fully qualified namespace name."); + } else { + $ns = substr($class, 0, $pos); + $path = Yii::getAlias('@' . str_replace('\\', '/', $ns), false); + if ($path === false) { + $this->addError($attribute, "The class namespace is invalid: $ns"); + } elseif (!is_dir($path)) { + $this->addError($attribute, "Please make sure the directory containing this class exists: $path"); + } + } + } + + /** + * @param string $value the attribute to be validated + * @return boolean whether the value is a reserved PHP keyword. + */ + public function isReservedKeyword($value) + { + static $keywords = [ + '__class__', + '__dir__', + '__file__', + '__function__', + '__line__', + '__method__', + '__namespace__', + '__trait__', + 'abstract', + 'and', + 'array', + 'as', + 'break', + 'case', + 'catch', + 'callable', + 'cfunction', + 'class', + 'clone', + 'const', + 'continue', + 'declare', + 'default', + 'die', + 'do', + 'echo', + 'else', + 'elseif', + 'empty', + 'enddeclare', + 'endfor', + 'endforeach', + 'endif', + 'endswitch', + 'endwhile', + 'eval', + 'exception', + 'exit', + 'extends', + 'final', + 'finally', + 'for', + 'foreach', + 'function', + 'global', + 'goto', + 'if', + 'implements', + 'include', + 'include_once', + 'instanceof', + 'insteadof', + 'interface', + 'isset', + 'list', + 'namespace', + 'new', + 'old_function', + 'or', + 'parent', + 'php_user_filter', + 'print', + 'private', + 'protected', + 'public', + 'require', + 'require_once', + 'return', + 'static', + 'switch', + 'this', + 'throw', + 'trait', + 'try', + 'unset', + 'use', + 'var', + 'while', + 'xor', + ]; + + return in_array(strtolower($value), $keywords, true); + } } diff --git a/extensions/gii/GiiAsset.php b/extensions/gii/GiiAsset.php index b100750f0ba..3eeb6734259 100644 --- a/extensions/gii/GiiAsset.php +++ b/extensions/gii/GiiAsset.php @@ -17,30 +17,30 @@ */ class GiiAsset extends AssetBundle { - /** - * @inheritdoc - */ - public $sourcePath = '@yii/gii/assets'; - /** - * @inheritdoc - */ - public $css = [ - 'main.css', - 'typeahead.js-bootstrap.css', - ]; - /** - * @inheritdoc - */ - public $js = [ - 'gii.js', - 'typeahead.js', - ]; - /** - * @inheritdoc - */ - public $depends = [ - 'yii\web\YiiAsset', - 'yii\bootstrap\BootstrapAsset', - 'yii\bootstrap\BootstrapPluginAsset', - ]; + /** + * @inheritdoc + */ + public $sourcePath = '@yii/gii/assets'; + /** + * @inheritdoc + */ + public $css = [ + 'main.css', + 'typeahead.js-bootstrap.css', + ]; + /** + * @inheritdoc + */ + public $js = [ + 'gii.js', + 'typeahead.js', + ]; + /** + * @inheritdoc + */ + public $depends = [ + 'yii\web\YiiAsset', + 'yii\bootstrap\BootstrapAsset', + 'yii\bootstrap\BootstrapPluginAsset', + ]; } diff --git a/extensions/gii/Module.php b/extensions/gii/Module.php index 0b0f6e4d6e1..b6936b036ce 100644 --- a/extensions/gii/Module.php +++ b/extensions/gii/Module.php @@ -53,95 +53,95 @@ */ class Module extends \yii\base\Module { - /** - * @inheritdoc - */ - public $controllerNamespace = 'yii\gii\controllers'; - /** - * @var array the list of IPs that are allowed to access this module. - * Each array element represents a single IP filter which can be either an IP address - * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment. - * The default value is `['127.0.0.1', '::1']`, which means the module can only be accessed - * by localhost. - */ - public $allowedIPs = ['127.0.0.1', '::1']; - /** - * @var array|Generator[] a list of generator configurations or instances. The array keys - * are the generator IDs (e.g. "crud"), and the array elements are the corresponding generator - * configurations or the instances. - * - * After the module is initialized, this property will become an array of generator instances - * which are created based on the configurations previously taken by this property. - * - * Newly assigned generators will be merged with the [[coreGenerators()|core ones]], and the former - * takes precedence in case when they have the same generator ID. - */ - public $generators = []; - /** - * @var integer the permission to be set for newly generated code files. - * This value will be used by PHP chmod function. - * Defaults to 0666, meaning the file is read-writable by all users. - */ - public $newFileMode = 0666; - /** - * @var integer the permission to be set for newly generated directories. - * This value will be used by PHP chmod function. - * Defaults to 0777, meaning the directory can be read, written and executed by all users. - */ - public $newDirMode = 0777; + /** + * @inheritdoc + */ + public $controllerNamespace = 'yii\gii\controllers'; + /** + * @var array the list of IPs that are allowed to access this module. + * Each array element represents a single IP filter which can be either an IP address + * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment. + * The default value is `['127.0.0.1', '::1']`, which means the module can only be accessed + * by localhost. + */ + public $allowedIPs = ['127.0.0.1', '::1']; + /** + * @var array|Generator[] a list of generator configurations or instances. The array keys + * are the generator IDs (e.g. "crud"), and the array elements are the corresponding generator + * configurations or the instances. + * + * After the module is initialized, this property will become an array of generator instances + * which are created based on the configurations previously taken by this property. + * + * Newly assigned generators will be merged with the [[coreGenerators()|core ones]], and the former + * takes precedence in case when they have the same generator ID. + */ + public $generators = []; + /** + * @var integer the permission to be set for newly generated code files. + * This value will be used by PHP chmod function. + * Defaults to 0666, meaning the file is read-writable by all users. + */ + public $newFileMode = 0666; + /** + * @var integer the permission to be set for newly generated directories. + * This value will be used by PHP chmod function. + * Defaults to 0777, meaning the directory can be read, written and executed by all users. + */ + public $newDirMode = 0777; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + foreach (array_merge($this->coreGenerators(), $this->generators) as $id => $config) { + $this->generators[$id] = Yii::createObject($config); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - foreach (array_merge($this->coreGenerators(), $this->generators) as $id => $config) { - $this->generators[$id] = Yii::createObject($config); - } - } + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if ($this->checkAccess()) { + return parent::beforeAction($action); + } else { + throw new ForbiddenHttpException('You are not allowed to access this page.'); + } + } - /** - * @inheritdoc - */ - public function beforeAction($action) - { - if ($this->checkAccess()) { - return parent::beforeAction($action); - } else { - throw new ForbiddenHttpException('You are not allowed to access this page.'); - } - } + /** + * @return boolean whether the module can be accessed by the current user + */ + protected function checkAccess() + { + $ip = Yii::$app->getRequest()->getUserIP(); + foreach ($this->allowedIPs as $filter) { + if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) { + return true; + } + } + Yii::warning('Access to Gii is denied due to IP address restriction. The requested IP is ' . $ip, __METHOD__); - /** - * @return boolean whether the module can be accessed by the current user - */ - protected function checkAccess() - { - $ip = Yii::$app->getRequest()->getUserIP(); - foreach ($this->allowedIPs as $filter) { - if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) { - return true; - } - } - Yii::warning('Access to Gii is denied due to IP address restriction. The requested IP is ' . $ip, __METHOD__); - return false; - } + return false; + } - /** - * Returns the list of the core code generator configurations. - * @return array the list of the core code generator configurations. - */ - protected function coreGenerators() - { - return [ - 'model' => ['class' => 'yii\gii\generators\model\Generator'], - 'crud' => ['class' => 'yii\gii\generators\crud\Generator'], - 'controller' => ['class' => 'yii\gii\generators\controller\Generator'], - 'form' => ['class' => 'yii\gii\generators\form\Generator'], - 'module' => ['class' => 'yii\gii\generators\module\Generator'], - 'extension' => ['class' => 'yii\gii\generators\extension\Generator'], - ]; - } + /** + * Returns the list of the core code generator configurations. + * @return array the list of the core code generator configurations. + */ + protected function coreGenerators() + { + return [ + 'model' => ['class' => 'yii\gii\generators\model\Generator'], + 'crud' => ['class' => 'yii\gii\generators\crud\Generator'], + 'controller' => ['class' => 'yii\gii\generators\controller\Generator'], + 'form' => ['class' => 'yii\gii\generators\form\Generator'], + 'module' => ['class' => 'yii\gii\generators\module\Generator'], + 'extension' => ['class' => 'yii\gii\generators\extension\Generator'], + ]; + } } diff --git a/extensions/gii/components/ActiveField.php b/extensions/gii/components/ActiveField.php index d128aa28d4b..13fc8289540 100644 --- a/extensions/gii/components/ActiveField.php +++ b/extensions/gii/components/ActiveField.php @@ -16,57 +16,59 @@ */ class ActiveField extends \yii\widgets\ActiveField { - /** - * @var Generator - */ - public $model; + /** + * @var Generator + */ + public $model; - /** - * @inheritdoc - */ - public function init() - { - $stickyAttributes = $this->model->stickyAttributes(); - if (in_array($this->attribute, $stickyAttributes)) { - $this->sticky(); - } - $hints = $this->model->hints(); - if (isset($hints[$this->attribute])) { - $this->hint($hints[$this->attribute]); - } - $autoCompleteData = $this->model->autoCompleteData(); - if (isset($autoCompleteData[$this->attribute])) { - if (is_callable($autoCompleteData[$this->attribute])) { - $this->autoComplete(call_user_func($autoCompleteData[$this->attribute])); - } else { - $this->autoComplete($autoCompleteData[$this->attribute]); - } - } - } + /** + * @inheritdoc + */ + public function init() + { + $stickyAttributes = $this->model->stickyAttributes(); + if (in_array($this->attribute, $stickyAttributes)) { + $this->sticky(); + } + $hints = $this->model->hints(); + if (isset($hints[$this->attribute])) { + $this->hint($hints[$this->attribute]); + } + $autoCompleteData = $this->model->autoCompleteData(); + if (isset($autoCompleteData[$this->attribute])) { + if (is_callable($autoCompleteData[$this->attribute])) { + $this->autoComplete(call_user_func($autoCompleteData[$this->attribute])); + } else { + $this->autoComplete($autoCompleteData[$this->attribute]); + } + } + } - /** - * Makes field remember its value between page reloads - * @return static the field object itself - */ - public function sticky() - { - $this->options['class'] .= ' sticky'; - return $this; - } + /** + * Makes field remember its value between page reloads + * @return static the field object itself + */ + public function sticky() + { + $this->options['class'] .= ' sticky'; - /** - * Makes field auto completable - * @param array $data auto complete data (array of callables or scalars) - * @return static the field object itself - */ - public function autoComplete($data) - { - static $counter = 0; - $this->inputOptions['class'] .= ' typeahead-' . (++$counter); - foreach ($data as &$item) { - $item = ['word' => $item]; - } - $this->form->getView()->registerJs("yii.gii.autocomplete($counter, " . Json::encode($data) . ");"); - return $this; - } + return $this; + } + + /** + * Makes field auto completable + * @param array $data auto complete data (array of callables or scalars) + * @return static the field object itself + */ + public function autoComplete($data) + { + static $counter = 0; + $this->inputOptions['class'] .= ' typeahead-' . (++$counter); + foreach ($data as &$item) { + $item = ['word' => $item]; + } + $this->form->getView()->registerJs("yii.gii.autocomplete($counter, " . Json::encode($data) . ");"); + + return $this; + } } diff --git a/extensions/gii/components/DiffRendererHtmlInline.php b/extensions/gii/components/DiffRendererHtmlInline.php index 908630af95f..dbaf007bce4 100644 --- a/extensions/gii/components/DiffRendererHtmlInline.php +++ b/extensions/gii/components/DiffRendererHtmlInline.php @@ -15,121 +15,122 @@ */ class DiffRendererHtmlInline extends \Diff_Renderer_Html_Array { - /** - * Render a and return diff with changes between the two sequences - * displayed inline (under each other) - * - * @return string The generated inline diff. - */ - public function render() - { - $changes = parent::render(); - $html = ''; - if (empty($changes)) { - return $html; - } + /** + * Render a and return diff with changes between the two sequences + * displayed inline (under each other) + * + * @return string The generated inline diff. + */ + public function render() + { + $changes = parent::render(); + $html = ''; + if (empty($changes)) { + return $html; + } - $html .= << - - - Old - New - Differences - - + + + Old + New + Differences + + HTML; - foreach ($changes as $i => $blocks) { - // If this is a separate block, we're condensing code so output ..., - // indicating a significant portion of the code has been collapsed as - // it is the same - if ($i > 0) { - $html .= << - - -   - + foreach ($changes as $i => $blocks) { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as + // it is the same + if ($i > 0) { + $html .= << + + +   + HTML; - } + } - foreach ($blocks as $change) { - $tag = ucfirst($change['tag']); - $html .= << + foreach ($blocks as $change) { + $tag = ucfirst($change['tag']); + $html .= << HTML; - // Equal changes should be shown on both sides of the diff - if ($change['tag'] === 'equal') { - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $toLine = $change['changed']['offset'] + $no + 1; - $html .= << - - - {$line} - + // Equal changes should be shown on both sides of the diff + if ($change['tag'] === 'equal') { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= << + + + {$line} + HTML; - } - } - // Added lines only on the right side - elseif ($change['tag'] === 'insert') { - foreach ($change['changed']['lines'] as $no => $line) { - $toLine = $change['changed']['offset'] + $no + 1; - $html .= << - - - {$line}  - + } + } + // Added lines only on the right side + elseif ($change['tag'] === 'insert') { + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= << + + + {$line}  + HTML; - } - } - // Show deleted lines only on the left side - elseif ($change['tag'] === 'delete') { - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $html .= << - - - {$line}  - + } + } + // Show deleted lines only on the left side + elseif ($change['tag'] === 'delete') { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= << + + + {$line}  + HTML; - } - } - // Show modified lines on both sides - elseif ($change['tag'] === 'replace') { - foreach ($change['base']['lines'] as $no => $line) { - $fromLine = $change['base']['offset'] + $no + 1; - $html .= << - - - {$line} - + } + } + // Show modified lines on both sides + elseif ($change['tag'] === 'replace') { + foreach ($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= << + + + {$line} + HTML; - } + } - foreach ($change['changed']['lines'] as $no => $line) { - $toLine = $change['changed']['offset'] + $no + 1; - $html .= << - - - {$line} - + foreach ($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= << + + + {$line} + HTML; - } - } - $html .= << + } + } + $html .= << HTML; - } - } - $html .= << HTML; - return $html; - } + + return $html; + } } diff --git a/extensions/gii/controllers/DefaultController.php b/extensions/gii/controllers/DefaultController.php index 31b4f53d98f..a5cb57ef45d 100644 --- a/extensions/gii/controllers/DefaultController.php +++ b/extensions/gii/controllers/DefaultController.php @@ -17,111 +17,113 @@ */ class DefaultController extends Controller { - public $layout = 'generator'; - /** - * @var \yii\gii\Module - */ - public $module; - /** - * @var \yii\gii\Generator - */ - public $generator; + public $layout = 'generator'; + /** + * @var \yii\gii\Module + */ + public $module; + /** + * @var \yii\gii\Generator + */ + public $generator; - public function actionIndex() - { - $this->layout = 'main'; - return $this->render('index'); - } + public function actionIndex() + { + $this->layout = 'main'; - public function actionView($id) - { - $generator = $this->loadGenerator($id); - $params = ['generator' => $generator, 'id' => $id]; - if (isset($_POST['preview']) || isset($_POST['generate'])) { - if ($generator->validate()) { - $generator->saveStickyAttributes(); - $files = $generator->generate(); - if (isset($_POST['generate']) && !empty($_POST['answers'])) { - $params['hasError'] = $generator->save($files, (array)$_POST['answers'], $results); - $params['results'] = $results; - } else { - $params['files'] = $files; - $params['answers'] = isset($_POST['answers']) ? $_POST['answers'] : null; - } - } - } + return $this->render('index'); + } - return $this->render('view', $params); - } + public function actionView($id) + { + $generator = $this->loadGenerator($id); + $params = ['generator' => $generator, 'id' => $id]; + if (isset($_POST['preview']) || isset($_POST['generate'])) { + if ($generator->validate()) { + $generator->saveStickyAttributes(); + $files = $generator->generate(); + if (isset($_POST['generate']) && !empty($_POST['answers'])) { + $params['hasError'] = $generator->save($files, (array) $_POST['answers'], $results); + $params['results'] = $results; + } else { + $params['files'] = $files; + $params['answers'] = isset($_POST['answers']) ? $_POST['answers'] : null; + } + } + } - public function actionPreview($id, $file) - { - $generator = $this->loadGenerator($id); - if ($generator->validate()) { - foreach ($generator->generate() as $f) { - if ($f->id === $file) { - $content = $f->preview(); - if ($content !== false) { - return '
    ' . $content . ''; - } else { - return '
    Preview is not available for this file type.
    '; - } - } - } - } - throw new NotFoundHttpException("Code file not found: $file"); - } + return $this->render('view', $params); + } - public function actionDiff($id, $file) - { - $generator = $this->loadGenerator($id); - if ($generator->validate()) { - foreach ($generator->generate() as $f) { - if ($f->id === $file) { - return $this->renderPartial('diff', [ - 'diff' => $f->diff(), - ]); - } - } - } - throw new NotFoundHttpException("Code file not found: $file"); - } + public function actionPreview($id, $file) + { + $generator = $this->loadGenerator($id); + if ($generator->validate()) { + foreach ($generator->generate() as $f) { + if ($f->id === $file) { + $content = $f->preview(); + if ($content !== false) { + return '
    ' . $content . ''; + } else { + return '
    Preview is not available for this file type.
    '; + } + } + } + } + throw new NotFoundHttpException("Code file not found: $file"); + } - /** - * Runs an action defined in the generator. - * Given an action named "xyz", the method "actionXyz()" in the generator will be called. - * If the method does not exist, a 400 HTTP exception will be thrown. - * @param string $id the ID of the generator - * @param string $name the action name - * @return mixed the result of the action. - * @throws NotFoundHttpException if the action method does not exist. - */ - public function actionAction($id, $name) - { - $generator = $this->loadGenerator($id); - $method = 'action' . $name; - if (method_exists($generator, $method)) { - return $generator->$method(); - } else { - throw new NotFoundHttpException("Unknown generator action: $name"); - } - } + public function actionDiff($id, $file) + { + $generator = $this->loadGenerator($id); + if ($generator->validate()) { + foreach ($generator->generate() as $f) { + if ($f->id === $file) { + return $this->renderPartial('diff', [ + 'diff' => $f->diff(), + ]); + } + } + } + throw new NotFoundHttpException("Code file not found: $file"); + } - /** - * Loads the generator with the specified ID. - * @param string $id the ID of the generator to be loaded. - * @return \yii\gii\Generator the loaded generator - * @throws NotFoundHttpException - */ - protected function loadGenerator($id) - { - if (isset($this->module->generators[$id])) { - $this->generator = $this->module->generators[$id]; - $this->generator->loadStickyAttributes(); - $this->generator->load($_POST); - return $this->generator; - } else { - throw new NotFoundHttpException("Code generator not found: $id"); - } - } + /** + * Runs an action defined in the generator. + * Given an action named "xyz", the method "actionXyz()" in the generator will be called. + * If the method does not exist, a 400 HTTP exception will be thrown. + * @param string $id the ID of the generator + * @param string $name the action name + * @return mixed the result of the action. + * @throws NotFoundHttpException if the action method does not exist. + */ + public function actionAction($id, $name) + { + $generator = $this->loadGenerator($id); + $method = 'action' . $name; + if (method_exists($generator, $method)) { + return $generator->$method(); + } else { + throw new NotFoundHttpException("Unknown generator action: $name"); + } + } + + /** + * Loads the generator with the specified ID. + * @param string $id the ID of the generator to be loaded. + * @return \yii\gii\Generator the loaded generator + * @throws NotFoundHttpException + */ + protected function loadGenerator($id) + { + if (isset($this->module->generators[$id])) { + $this->generator = $this->module->generators[$id]; + $this->generator->loadStickyAttributes(); + $this->generator->load($_POST); + + return $this->generator; + } else { + throw new NotFoundHttpException("Code generator not found: $id"); + } + } } diff --git a/extensions/gii/generators/controller/Generator.php b/extensions/gii/generators/controller/Generator.php index 0e2d8ff79dd..7393397ec12 100644 --- a/extensions/gii/generators/controller/Generator.php +++ b/extensions/gii/generators/controller/Generator.php @@ -29,217 +29,222 @@ */ class Generator extends \yii\gii\Generator { - /** - * @var string the controller ID - */ - public $controller; - /** - * @var string the base class of the controller - */ - public $baseClass = 'yii\web\Controller'; - /** - * @var string the namespace of the controller class - */ - public $ns; - /** - * @var string list of action IDs separated by commas or spaces - */ - public $actions = 'index'; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - $this->ns = \Yii::$app->controllerNamespace; - } - - /** - * @inheritdoc - */ - public function getName() - { - return 'Controller Generator'; - } - - /** - * @inheritdoc - */ - public function getDescription() - { - return 'This generator helps you to quickly generate a new controller class, - one or several controller actions and their corresponding views.'; - } - - /** - * @inheritdoc - */ - public function rules() - { - return array_merge(parent::rules(), [ - [['controller', 'actions', 'baseClass', 'ns'], 'filter', 'filter' => 'trim'], - [['controller', 'baseClass'], 'required'], - [['controller'], 'match', 'pattern' => '/^[a-z][a-z0-9\\-\\/]*$/', 'message' => 'Only a-z, 0-9, dashes (-) and slashes (/) are allowed.'], - [['actions'], 'match', 'pattern' => '/^[a-z][a-z0-9\\-,\\s]*$/', 'message' => 'Only a-z, 0-9, dashes (-), spaces and commas are allowed.'], - [['baseClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], - [['ns'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], - ]); - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'baseClass' => 'Base Class', - 'controller' => 'Controller ID', - 'actions' => 'Action IDs', - 'ns' => 'Controller Namespace', - ]; - } - - /** - * @inheritdoc - */ - public function requiredTemplates() - { - return [ - 'controller.php', - 'view.php', - ]; - } - - /** - * @inheritdoc - */ - public function stickyAttributes() - { - return ['ns', 'baseClass']; - } - - /** - * @inheritdoc - */ - public function hints() - { - return [ - 'controller' => 'Controller ID should be in lower case and may contain module ID(s) separated by slashes. For example: -
      -
    • order generates OrderController.php
    • -
    • order-item generates OrderItemController.php
    • -
    • admin/user generates UserController.php within the admin module.
    • -
    ', - 'actions' => 'Provide one or multiple action IDs to generate empty action method(s) in the controller. Separate multiple action IDs with commas or spaces. - Action IDs should be in lower case. For example: -
      -
    • index generates actionIndex()
    • -
    • create-order generates actionCreateOrder()
    • -
    ', - 'ns' => 'This is the namespace that the new controller class will use.', - 'baseClass' => 'This is the class that the new controller class will extend from. Please make sure the class exists and can be autoloaded.', - ]; - } - - /** - * @inheritdoc - */ - public function successMessage() - { - $actions = $this->getActionIDs(); - if (in_array('index', $actions)) { - $route = $this->controller . '/index'; - } else { - $route = $this->controller . '/' . reset($actions); - } - $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($route), ['target' => '_blank']); - return "The controller has been generated successfully. You may $link."; - } - - /** - * @inheritdoc - */ - public function generate() - { - $files = []; - - $files[] = new CodeFile( - $this->getControllerFile(), - $this->render('controller.php') - ); - - foreach ($this->getActionIDs() as $action) { - $files[] = new CodeFile( - $this->getViewFile($action), - $this->render('view.php', ['action' => $action]) - ); - } - - return $files; - } - - /** - * Normalizes [[actions]] into an array of action IDs. - * @return array an array of action IDs entered by the user - */ - public function getActionIDs() - { - $actions = array_unique(preg_split('/[\s,]+/', $this->actions, -1, PREG_SPLIT_NO_EMPTY)); - sort($actions); - return $actions; - } - - /** - * @return string the controller class name without the namespace part. - */ - public function getControllerClass() - { - return Inflector::id2camel($this->getControllerID()) . 'Controller'; - } - - /** - * @return string the controller ID (without the module ID prefix) - */ - public function getControllerID() - { - if (($pos = strrpos($this->controller, '/')) !== false) { - return substr($this->controller, $pos + 1); - } else { - return $this->controller; - } - } - - /** - * @return \yii\base\Module the module that the new controller belongs to - */ - public function getModule() - { - if (($pos = strrpos($this->controller, '/')) !== false) { - $id = substr($this->controller, 0, $pos); - if (($module = Yii::$app->getModule($id)) !== null) { - return $module; - } - } - return Yii::$app; - } - - /** - * @return string the controller class file path - */ - public function getControllerFile() - { - $module = $this->getModule(); - return $module->getControllerPath() . '/' . $this->getControllerClass() . '.php'; - } - - /** - * @param string $action the action ID - * @return string the action view file path - */ - public function getViewFile($action) - { - $module = $this->getModule(); - return $module->getViewPath() . '/' . $this->getControllerID() . '/' . $action . '.php'; - } + /** + * @var string the controller ID + */ + public $controller; + /** + * @var string the base class of the controller + */ + public $baseClass = 'yii\web\Controller'; + /** + * @var string the namespace of the controller class + */ + public $ns; + /** + * @var string list of action IDs separated by commas or spaces + */ + public $actions = 'index'; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->ns = \Yii::$app->controllerNamespace; + } + + /** + * @inheritdoc + */ + public function getName() + { + return 'Controller Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator helps you to quickly generate a new controller class, + one or several controller actions and their corresponding views.'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), [ + [['controller', 'actions', 'baseClass', 'ns'], 'filter', 'filter' => 'trim'], + [['controller', 'baseClass'], 'required'], + [['controller'], 'match', 'pattern' => '/^[a-z][a-z0-9\\-\\/]*$/', 'message' => 'Only a-z, 0-9, dashes (-) and slashes (/) are allowed.'], + [['actions'], 'match', 'pattern' => '/^[a-z][a-z0-9\\-,\\s]*$/', 'message' => 'Only a-z, 0-9, dashes (-), spaces and commas are allowed.'], + [['baseClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], + [['ns'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], + ]); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'baseClass' => 'Base Class', + 'controller' => 'Controller ID', + 'actions' => 'Action IDs', + 'ns' => 'Controller Namespace', + ]; + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return [ + 'controller.php', + 'view.php', + ]; + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return ['ns', 'baseClass']; + } + + /** + * @inheritdoc + */ + public function hints() + { + return [ + 'controller' => 'Controller ID should be in lower case and may contain module ID(s) separated by slashes. For example: +
      +
    • order generates OrderController.php
    • +
    • order-item generates OrderItemController.php
    • +
    • admin/user generates UserController.php within the admin module.
    • +
    ', + 'actions' => 'Provide one or multiple action IDs to generate empty action method(s) in the controller. Separate multiple action IDs with commas or spaces. + Action IDs should be in lower case. For example: +
      +
    • index generates actionIndex()
    • +
    • create-order generates actionCreateOrder()
    • +
    ', + 'ns' => 'This is the namespace that the new controller class will use.', + 'baseClass' => 'This is the class that the new controller class will extend from. Please make sure the class exists and can be autoloaded.', + ]; + } + + /** + * @inheritdoc + */ + public function successMessage() + { + $actions = $this->getActionIDs(); + if (in_array('index', $actions)) { + $route = $this->controller . '/index'; + } else { + $route = $this->controller . '/' . reset($actions); + } + $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($route), ['target' => '_blank']); + + return "The controller has been generated successfully. You may $link."; + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = []; + + $files[] = new CodeFile( + $this->getControllerFile(), + $this->render('controller.php') + ); + + foreach ($this->getActionIDs() as $action) { + $files[] = new CodeFile( + $this->getViewFile($action), + $this->render('view.php', ['action' => $action]) + ); + } + + return $files; + } + + /** + * Normalizes [[actions]] into an array of action IDs. + * @return array an array of action IDs entered by the user + */ + public function getActionIDs() + { + $actions = array_unique(preg_split('/[\s,]+/', $this->actions, -1, PREG_SPLIT_NO_EMPTY)); + sort($actions); + + return $actions; + } + + /** + * @return string the controller class name without the namespace part. + */ + public function getControllerClass() + { + return Inflector::id2camel($this->getControllerID()) . 'Controller'; + } + + /** + * @return string the controller ID (without the module ID prefix) + */ + public function getControllerID() + { + if (($pos = strrpos($this->controller, '/')) !== false) { + return substr($this->controller, $pos + 1); + } else { + return $this->controller; + } + } + + /** + * @return \yii\base\Module the module that the new controller belongs to + */ + public function getModule() + { + if (($pos = strrpos($this->controller, '/')) !== false) { + $id = substr($this->controller, 0, $pos); + if (($module = Yii::$app->getModule($id)) !== null) { + return $module; + } + } + + return Yii::$app; + } + + /** + * @return string the controller class file path + */ + public function getControllerFile() + { + $module = $this->getModule(); + + return $module->getControllerPath() . '/' . $this->getControllerClass() . '.php'; + } + + /** + * @param string $action the action ID + * @return string the action view file path + */ + public function getViewFile($action) + { + $module = $this->getModule(); + + return $module->getViewPath() . '/' . $this->getControllerID() . '/' . $action . '.php'; + } } diff --git a/extensions/gii/generators/controller/templates/controller.php b/extensions/gii/generators/controller/templates/controller.php index 95358d21405..8447809314f 100644 --- a/extensions/gii/generators/controller/templates/controller.php +++ b/extensions/gii/generators/controller/templates/controller.php @@ -19,10 +19,10 @@ class getControllerClass() ?> extends baseClass, '\\') . "\n" ?> { getActionIDs() as $action): ?> - public function action() - { - return $this->render(''); - } + public function action() + { + return $this->render(''); + } } diff --git a/extensions/gii/generators/controller/templates/view.php b/extensions/gii/generators/controller/templates/view.php index a458b16ef77..611cb1be75a 100644 --- a/extensions/gii/generators/controller/templates/view.php +++ b/extensions/gii/generators/controller/templates/view.php @@ -17,6 +17,6 @@

    getControllerID() . '/' . $action ?>

    - You may change the content of this page by modifying - the file __FILE__; ?>. + You may change the content of this page by modifying + the file __FILE__; ?>.

    diff --git a/extensions/gii/generators/crud/Generator.php b/extensions/gii/generators/crud/Generator.php index 116f8395f41..b1ca62d167e 100644 --- a/extensions/gii/generators/crud/Generator.php +++ b/extensions/gii/generators/crud/Generator.php @@ -30,473 +30,480 @@ */ class Generator extends \yii\gii\Generator { - public $modelClass; - public $moduleID; - public $controllerClass; - public $baseControllerClass = 'yii\web\Controller'; - public $indexWidgetType = 'grid'; - public $searchModelClass; - - /** - * @inheritdoc - */ - public function getName() - { - return 'CRUD Generator'; - } - - /** - * @inheritdoc - */ - public function getDescription() - { - return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete) - operations for the specified data model.'; - } - - /** - * @inheritdoc - */ - public function rules() - { - return array_merge(parent::rules(), [ - [['moduleID', 'controllerClass', 'modelClass', 'searchModelClass', 'baseControllerClass'], 'filter', 'filter' => 'trim'], - [['modelClass', 'searchModelClass', 'controllerClass', 'baseControllerClass', 'indexWidgetType'], 'required'], - [['searchModelClass'], 'compare', 'compareAttribute' => 'modelClass', 'operator' => '!==', 'message' => 'Search Model Class must not be equal to Model Class.'], - [['modelClass', 'controllerClass', 'baseControllerClass', 'searchModelClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], - [['modelClass'], 'validateClass', 'params' => ['extends' => BaseActiveRecord::className()]], - [['baseControllerClass'], 'validateClass', 'params' => ['extends' => Controller::className()]], - [['controllerClass'], 'match', 'pattern' => '/Controller$/', 'message' => 'Controller class name must be suffixed with "Controller".'], - [['controllerClass', 'searchModelClass'], 'validateNewClass'], - [['indexWidgetType'], 'in', 'range' => ['grid', 'list']], - [['modelClass'], 'validateModelClass'], - [['moduleID'], 'validateModuleID'], - ]); - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return array_merge(parent::attributeLabels(), [ - 'modelClass' => 'Model Class', - 'moduleID' => 'Module ID', - 'controllerClass' => 'Controller Class', - 'baseControllerClass' => 'Base Controller Class', - 'indexWidgetType' => 'Widget Used in Index Page', - 'searchModelClass' => 'Search Model Class', - ]); - } - - /** - * @inheritdoc - */ - public function hints() - { - return [ - 'modelClass' => 'This is the ActiveRecord class associated with the table that CRUD will be built upon. - You should provide a fully qualified class name, e.g., app\models\Post.', - 'controllerClass' => 'This is the name of the controller class to be generated. You should - provide a fully qualified namespaced class, .e.g, app\controllers\PostController.', - 'baseControllerClass' => 'This is the class that the new CRUD controller class will extend from. - You should provide a fully qualified class name, e.g., yii\web\Controller.', - 'moduleID' => 'This is the ID of the module that the generated controller will belong to. - If not set, it means the controller will belong to the application.', - 'indexWidgetType' => 'This is the widget type to be used in the index page to display list of the models. - You may choose either GridView or ListView', - 'searchModelClass' => 'This is the name of the search model class to be generated. You should provide a fully - qualified namespaced class name, e.g., app\models\search\PostSearch.', - ]; - } - - /** - * @inheritdoc - */ - public function requiredTemplates() - { - return ['controller.php']; - } - - /** - * @inheritdoc - */ - public function stickyAttributes() - { - return ['baseControllerClass', 'moduleID', 'indexWidgetType']; - } - - /** - * Checks if model class is valid - */ - public function validateModelClass() - { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $pk = $class::primaryKey(); - if (empty($pk)) { - $this->addError('modelClass', "The table associated with $class must have primary key(s)."); - } - } - - /** - * Checks if model ID is valid - */ - public function validateModuleID() - { - if (!empty($this->moduleID)) { - $module = Yii::$app->getModule($this->moduleID); - if ($module === null) { - $this->addError('moduleID', "Module '{$this->moduleID}' does not exist."); - } - } - } - - /** - * @inheritdoc - */ - public function generate() - { - $controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php'); - $searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php')); - $files = [ - new CodeFile($controllerFile, $this->render('controller.php')), - new CodeFile($searchModel, $this->render('search.php')), - ]; - - $viewPath = $this->getViewPath(); - $templatePath = $this->getTemplatePath() . '/views'; - foreach (scandir($templatePath) as $file) { - if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') { - $files[] = new CodeFile("$viewPath/$file", $this->render("views/$file")); - } - } - - - return $files; - } - - /** - * @return string the controller ID (without the module ID prefix) - */ - public function getControllerID() - { - $pos = strrpos($this->controllerClass, '\\'); - $class = substr(substr($this->controllerClass, $pos + 1), 0, -10); - return Inflector::camel2id($class); - } - - /** - * @return string the action view file path - */ - public function getViewPath() - { - $module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID); - return $module->getViewPath() . '/' . $this->getControllerID() ; - } - - public function getNameAttribute() - { - foreach ($this->getColumnNames() as $name) { - if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) { - return $name; - } - } - /** @var \yii\db\ActiveRecord $class */ - $class = $this->modelClass; - $pk = $class::primaryKey(); - return $pk[0]; - } - - /** - * Generates code for active field - * @param string $attribute - * @return string - */ - public function generateActiveField($attribute) - { - $tableSchema = $this->getTableSchema(); - if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) { - if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) { - return "\$form->field(\$model, '$attribute')->passwordInput()"; - } else { - return "\$form->field(\$model, '$attribute')"; - } - } - $column = $tableSchema->columns[$attribute]; - if ($column->phpType === 'boolean') { - return "\$form->field(\$model, '$attribute')->checkbox()"; - } elseif ($column->type === 'text') { - return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])"; - } else { - if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) { - $input = 'passwordInput'; - } else { - $input = 'textInput'; - } - if ($column->phpType !== 'string' || $column->size === null) { - return "\$form->field(\$model, '$attribute')->$input()"; - } else { - return "\$form->field(\$model, '$attribute')->$input(['maxlength' => $column->size])"; - } - } - } - - /** - * Generates code for active search field - * @param string $attribute - * @return string - */ - public function generateActiveSearchField($attribute) - { - $tableSchema = $this->getTableSchema(); - if ($tableSchema === false) { - return "\$form->field(\$model, '$attribute')"; - } - $column = $tableSchema->columns[$attribute]; - if ($column->phpType === 'boolean') { - return "\$form->field(\$model, '$attribute')->checkbox()"; - } else { - return "\$form->field(\$model, '$attribute')"; - } - } - - /** - * Generates column format - * @param \yii\db\ColumnSchema $column - * @return string - */ - public function generateColumnFormat($column) - { - if ($column->phpType === 'boolean') { - return 'boolean'; - } elseif ($column->type === 'text') { - return 'ntext'; - } elseif (stripos($column->name, 'time') !== false && $column->phpType === 'integer') { - return 'datetime'; - } elseif (stripos($column->name, 'email') !== false) { - return 'email'; - } elseif (stripos($column->name, 'url') !== false) { - return 'url'; - } else { - return 'text'; - } - } - - /** - * Generates validation rules for the search model. - * @return array the generated validation rules - */ - public function generateSearchRules() - { - if (($table = $this->getTableSchema()) === false) { - return ["[['" . implode("', '", $this->getColumnNames()) . "'], 'safe']"]; - } - $types = []; - foreach ($table->columns as $column) { - switch ($column->type) { - case Schema::TYPE_SMALLINT: - case Schema::TYPE_INTEGER: - case Schema::TYPE_BIGINT: - $types['integer'][] = $column->name; - break; - case Schema::TYPE_BOOLEAN: - $types['boolean'][] = $column->name; - break; - case Schema::TYPE_FLOAT: - case Schema::TYPE_DECIMAL: - case Schema::TYPE_MONEY: - $types['number'][] = $column->name; - break; - case Schema::TYPE_DATE: - case Schema::TYPE_TIME: - case Schema::TYPE_DATETIME: - case Schema::TYPE_TIMESTAMP: - default: - $types['safe'][] = $column->name; - break; - } - } - - $rules = []; - foreach ($types as $type => $columns) { - $rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; - } - - return $rules; - } - - /** - * @return array searchable attributes - */ - public function getSearchAttributes() - { - return $this->getColumnNames(); - } - - /** - * Generates the attribute labels for the search model. - * @return array the generated attribute labels (name => label) - */ - public function generateSearchLabels() - { - /** @var \yii\base\Model $model */ - $model = new $this->modelClass(); - $attributeLabels = $model->attributeLabels(); - $labels = []; - foreach ($this->getColumnNames() as $name) { - if (isset($attributeLabels[$name])) { - $labels[$name] = $attributeLabels[$name]; - } else { - if (!strcasecmp($name, 'id')) { - $labels[$name] = 'ID'; - } else { - $label = Inflector::camel2words($name); - if (strcasecmp(substr($label, -3), ' id') === 0) { - $label = substr($label, 0, -3) . ' ID'; - } - $labels[$name] = $label; - } - } - } - return $labels; - } - - /** - * Generates search conditions - * @return array - */ - public function generateSearchConditions() - { - $columns = []; - if (($table = $this->getTableSchema()) === false) { - $class = $this->modelClass; - /** @var \yii\base\Model $model */ - $model = new $class(); - foreach ($model->attributes() as $attribute) { - $columns[$attribute] = 'unknown'; - } - } else { - foreach ($table->columns as $column) { - $columns[$column->name] = $column->type; - } - } - $conditions = []; - foreach ($columns as $column => $type) { - switch ($type) { - case Schema::TYPE_SMALLINT: - case Schema::TYPE_INTEGER: - case Schema::TYPE_BIGINT: - case Schema::TYPE_BOOLEAN: - case Schema::TYPE_FLOAT: - case Schema::TYPE_DECIMAL: - case Schema::TYPE_MONEY: - case Schema::TYPE_DATE: - case Schema::TYPE_TIME: - case Schema::TYPE_DATETIME: - case Schema::TYPE_TIMESTAMP: - $conditions[] = "\$this->addCondition(\$query, '{$column}');"; - break; - default: - $conditions[] = "\$this->addCondition(\$query, '{$column}', true);"; - break; - } - } - - return $conditions; - } - - /** - * Generates URL parameters - * @return string - */ - public function generateUrlParams() - { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $pks = $class::primaryKey(); - if (count($pks) === 1) { - return "'id' => \$model->{$pks[0]}"; - } else { - $params = []; - foreach ($pks as $pk) { - $params[] = "'$pk' => \$model->$pk"; - } - return implode(', ', $params); - } - } - - /** - * Generates action parameters - * @return string - */ - public function generateActionParams() - { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $pks = $class::primaryKey(); - if (count($pks) === 1) { - return '$id'; - } else { - return '$' . implode(', $', $pks); - } - } - - /** - * Generates parameter tags for phpdoc - * @return array parameter tags for phpdoc - */ - public function generateActionParamComments() - { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $pks = $class::primaryKey(); - if (($table = $this->getTableSchema()) === false) { - $params = []; - foreach ($pks as $pk) { - $params[] = '@param ' . (substr(strtolower($pk), -2) == 'id' ? 'integer' : 'string') . ' $' . $pk; - } - return $params; - } - if (count($pks) === 1) { - return ['@param ' . $table->columns[$pks[0]]->phpType . ' $id']; - } else { - $params = []; - foreach ($pks as $pk) { - $params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk; - } - return $params; - } - } - - /** - * Returns table schema for current model class or false if it is not an active record - * @return boolean|\yii\db\TableSchema - */ - public function getTableSchema() - { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - if (is_subclass_of($class, 'yii\db\ActiveRecord')) { - return $class::getTableSchema(); - } else { - return false; - } - } - - /** - * @return array model column names - */ - public function getColumnNames() - { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - if (is_subclass_of($class, 'yii\db\ActiveRecord')) { - return $class::getTableSchema()->getColumnNames(); - } else { - /** @var \yii\base\Model $model */ - $model = new $class(); - return $model->attributes(); - } - } + public $modelClass; + public $moduleID; + public $controllerClass; + public $baseControllerClass = 'yii\web\Controller'; + public $indexWidgetType = 'grid'; + public $searchModelClass; + + /** + * @inheritdoc + */ + public function getName() + { + return 'CRUD Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete) + operations for the specified data model.'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), [ + [['moduleID', 'controllerClass', 'modelClass', 'searchModelClass', 'baseControllerClass'], 'filter', 'filter' => 'trim'], + [['modelClass', 'searchModelClass', 'controllerClass', 'baseControllerClass', 'indexWidgetType'], 'required'], + [['searchModelClass'], 'compare', 'compareAttribute' => 'modelClass', 'operator' => '!==', 'message' => 'Search Model Class must not be equal to Model Class.'], + [['modelClass', 'controllerClass', 'baseControllerClass', 'searchModelClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], + [['modelClass'], 'validateClass', 'params' => ['extends' => BaseActiveRecord::className()]], + [['baseControllerClass'], 'validateClass', 'params' => ['extends' => Controller::className()]], + [['controllerClass'], 'match', 'pattern' => '/Controller$/', 'message' => 'Controller class name must be suffixed with "Controller".'], + [['controllerClass', 'searchModelClass'], 'validateNewClass'], + [['indexWidgetType'], 'in', 'range' => ['grid', 'list']], + [['modelClass'], 'validateModelClass'], + [['moduleID'], 'validateModuleID'], + ]); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return array_merge(parent::attributeLabels(), [ + 'modelClass' => 'Model Class', + 'moduleID' => 'Module ID', + 'controllerClass' => 'Controller Class', + 'baseControllerClass' => 'Base Controller Class', + 'indexWidgetType' => 'Widget Used in Index Page', + 'searchModelClass' => 'Search Model Class', + ]); + } + + /** + * @inheritdoc + */ + public function hints() + { + return [ + 'modelClass' => 'This is the ActiveRecord class associated with the table that CRUD will be built upon. + You should provide a fully qualified class name, e.g., app\models\Post.', + 'controllerClass' => 'This is the name of the controller class to be generated. You should + provide a fully qualified namespaced class, .e.g, app\controllers\PostController.', + 'baseControllerClass' => 'This is the class that the new CRUD controller class will extend from. + You should provide a fully qualified class name, e.g., yii\web\Controller.', + 'moduleID' => 'This is the ID of the module that the generated controller will belong to. + If not set, it means the controller will belong to the application.', + 'indexWidgetType' => 'This is the widget type to be used in the index page to display list of the models. + You may choose either GridView or ListView', + 'searchModelClass' => 'This is the name of the search model class to be generated. You should provide a fully + qualified namespaced class name, e.g., app\models\search\PostSearch.', + ]; + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return ['controller.php']; + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return ['baseControllerClass', 'moduleID', 'indexWidgetType']; + } + + /** + * Checks if model class is valid + */ + public function validateModelClass() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pk = $class::primaryKey(); + if (empty($pk)) { + $this->addError('modelClass', "The table associated with $class must have primary key(s)."); + } + } + + /** + * Checks if model ID is valid + */ + public function validateModuleID() + { + if (!empty($this->moduleID)) { + $module = Yii::$app->getModule($this->moduleID); + if ($module === null) { + $this->addError('moduleID', "Module '{$this->moduleID}' does not exist."); + } + } + } + + /** + * @inheritdoc + */ + public function generate() + { + $controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php'); + $searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php')); + $files = [ + new CodeFile($controllerFile, $this->render('controller.php')), + new CodeFile($searchModel, $this->render('search.php')), + ]; + + $viewPath = $this->getViewPath(); + $templatePath = $this->getTemplatePath() . '/views'; + foreach (scandir($templatePath) as $file) { + if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') { + $files[] = new CodeFile("$viewPath/$file", $this->render("views/$file")); + } + } + + return $files; + } + + /** + * @return string the controller ID (without the module ID prefix) + */ + public function getControllerID() + { + $pos = strrpos($this->controllerClass, '\\'); + $class = substr(substr($this->controllerClass, $pos + 1), 0, -10); + + return Inflector::camel2id($class); + } + + /** + * @return string the action view file path + */ + public function getViewPath() + { + $module = empty($this->moduleID) ? Yii::$app : Yii::$app->getModule($this->moduleID); + + return $module->getViewPath() . '/' . $this->getControllerID() ; + } + + public function getNameAttribute() + { + foreach ($this->getColumnNames() as $name) { + if (!strcasecmp($name, 'name') || !strcasecmp($name, 'title')) { + return $name; + } + } + /** @var \yii\db\ActiveRecord $class */ + $class = $this->modelClass; + $pk = $class::primaryKey(); + + return $pk[0]; + } + + /** + * Generates code for active field + * @param string $attribute + * @return string + */ + public function generateActiveField($attribute) + { + $tableSchema = $this->getTableSchema(); + if ($tableSchema === false || !isset($tableSchema->columns[$attribute])) { + if (preg_match('/^(password|pass|passwd|passcode)$/i', $attribute)) { + return "\$form->field(\$model, '$attribute')->passwordInput()"; + } else { + return "\$form->field(\$model, '$attribute')"; + } + } + $column = $tableSchema->columns[$attribute]; + if ($column->phpType === 'boolean') { + return "\$form->field(\$model, '$attribute')->checkbox()"; + } elseif ($column->type === 'text') { + return "\$form->field(\$model, '$attribute')->textarea(['rows' => 6])"; + } else { + if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name)) { + $input = 'passwordInput'; + } else { + $input = 'textInput'; + } + if ($column->phpType !== 'string' || $column->size === null) { + return "\$form->field(\$model, '$attribute')->$input()"; + } else { + return "\$form->field(\$model, '$attribute')->$input(['maxlength' => $column->size])"; + } + } + } + + /** + * Generates code for active search field + * @param string $attribute + * @return string + */ + public function generateActiveSearchField($attribute) + { + $tableSchema = $this->getTableSchema(); + if ($tableSchema === false) { + return "\$form->field(\$model, '$attribute')"; + } + $column = $tableSchema->columns[$attribute]; + if ($column->phpType === 'boolean') { + return "\$form->field(\$model, '$attribute')->checkbox()"; + } else { + return "\$form->field(\$model, '$attribute')"; + } + } + + /** + * Generates column format + * @param \yii\db\ColumnSchema $column + * @return string + */ + public function generateColumnFormat($column) + { + if ($column->phpType === 'boolean') { + return 'boolean'; + } elseif ($column->type === 'text') { + return 'ntext'; + } elseif (stripos($column->name, 'time') !== false && $column->phpType === 'integer') { + return 'datetime'; + } elseif (stripos($column->name, 'email') !== false) { + return 'email'; + } elseif (stripos($column->name, 'url') !== false) { + return 'url'; + } else { + return 'text'; + } + } + + /** + * Generates validation rules for the search model. + * @return array the generated validation rules + */ + public function generateSearchRules() + { + if (($table = $this->getTableSchema()) === false) { + return ["[['" . implode("', '", $this->getColumnNames()) . "'], 'safe']"]; + } + $types = []; + foreach ($table->columns as $column) { + switch ($column->type) { + case Schema::TYPE_SMALLINT: + case Schema::TYPE_INTEGER: + case Schema::TYPE_BIGINT: + $types['integer'][] = $column->name; + break; + case Schema::TYPE_BOOLEAN: + $types['boolean'][] = $column->name; + break; + case Schema::TYPE_FLOAT: + case Schema::TYPE_DECIMAL: + case Schema::TYPE_MONEY: + $types['number'][] = $column->name; + break; + case Schema::TYPE_DATE: + case Schema::TYPE_TIME: + case Schema::TYPE_DATETIME: + case Schema::TYPE_TIMESTAMP: + default: + $types['safe'][] = $column->name; + break; + } + } + + $rules = []; + foreach ($types as $type => $columns) { + $rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; + } + + return $rules; + } + + /** + * @return array searchable attributes + */ + public function getSearchAttributes() + { + return $this->getColumnNames(); + } + + /** + * Generates the attribute labels for the search model. + * @return array the generated attribute labels (name => label) + */ + public function generateSearchLabels() + { + /** @var \yii\base\Model $model */ + $model = new $this->modelClass(); + $attributeLabels = $model->attributeLabels(); + $labels = []; + foreach ($this->getColumnNames() as $name) { + if (isset($attributeLabels[$name])) { + $labels[$name] = $attributeLabels[$name]; + } else { + if (!strcasecmp($name, 'id')) { + $labels[$name] = 'ID'; + } else { + $label = Inflector::camel2words($name); + if (strcasecmp(substr($label, -3), ' id') === 0) { + $label = substr($label, 0, -3) . ' ID'; + } + $labels[$name] = $label; + } + } + } + + return $labels; + } + + /** + * Generates search conditions + * @return array + */ + public function generateSearchConditions() + { + $columns = []; + if (($table = $this->getTableSchema()) === false) { + $class = $this->modelClass; + /** @var \yii\base\Model $model */ + $model = new $class(); + foreach ($model->attributes() as $attribute) { + $columns[$attribute] = 'unknown'; + } + } else { + foreach ($table->columns as $column) { + $columns[$column->name] = $column->type; + } + } + $conditions = []; + foreach ($columns as $column => $type) { + switch ($type) { + case Schema::TYPE_SMALLINT: + case Schema::TYPE_INTEGER: + case Schema::TYPE_BIGINT: + case Schema::TYPE_BOOLEAN: + case Schema::TYPE_FLOAT: + case Schema::TYPE_DECIMAL: + case Schema::TYPE_MONEY: + case Schema::TYPE_DATE: + case Schema::TYPE_TIME: + case Schema::TYPE_DATETIME: + case Schema::TYPE_TIMESTAMP: + $conditions[] = "\$this->addCondition(\$query, '{$column}');"; + break; + default: + $conditions[] = "\$this->addCondition(\$query, '{$column}', true);"; + break; + } + } + + return $conditions; + } + + /** + * Generates URL parameters + * @return string + */ + public function generateUrlParams() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pks = $class::primaryKey(); + if (count($pks) === 1) { + return "'id' => \$model->{$pks[0]}"; + } else { + $params = []; + foreach ($pks as $pk) { + $params[] = "'$pk' => \$model->$pk"; + } + + return implode(', ', $params); + } + } + + /** + * Generates action parameters + * @return string + */ + public function generateActionParams() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pks = $class::primaryKey(); + if (count($pks) === 1) { + return '$id'; + } else { + return '$' . implode(', $', $pks); + } + } + + /** + * Generates parameter tags for phpdoc + * @return array parameter tags for phpdoc + */ + public function generateActionParamComments() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pks = $class::primaryKey(); + if (($table = $this->getTableSchema()) === false) { + $params = []; + foreach ($pks as $pk) { + $params[] = '@param ' . (substr(strtolower($pk), -2) == 'id' ? 'integer' : 'string') . ' $' . $pk; + } + + return $params; + } + if (count($pks) === 1) { + return ['@param ' . $table->columns[$pks[0]]->phpType . ' $id']; + } else { + $params = []; + foreach ($pks as $pk) { + $params[] = '@param ' . $table->columns[$pk]->phpType . ' $' . $pk; + } + + return $params; + } + } + + /** + * Returns table schema for current model class or false if it is not an active record + * @return boolean|\yii\db\TableSchema + */ + public function getTableSchema() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + if (is_subclass_of($class, 'yii\db\ActiveRecord')) { + return $class::getTableSchema(); + } else { + return false; + } + } + + /** + * @return array model column names + */ + public function getColumnNames() + { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + if (is_subclass_of($class, 'yii\db\ActiveRecord')) { + return $class::getTableSchema()->getColumnNames(); + } else { + /** @var \yii\base\Model $model */ + $model = new $class(); + + return $model->attributes(); + } + } } diff --git a/extensions/gii/generators/crud/form.php b/extensions/gii/generators/crud/form.php index 1b101c24896..36f4b283da0 100644 --- a/extensions/gii/generators/crud/form.php +++ b/extensions/gii/generators/crud/form.php @@ -11,6 +11,6 @@ echo $form->field($generator, 'baseControllerClass'); echo $form->field($generator, 'moduleID'); echo $form->field($generator, 'indexWidgetType')->dropDownList([ - 'grid' => 'GridView', - 'list' => 'ListView', + 'grid' => 'GridView', + 'list' => 'ListView', ]); diff --git a/extensions/gii/generators/crud/templates/controller.php b/extensions/gii/generators/crud/templates/controller.php index 3b8c10cdc77..033bbf20b65 100644 --- a/extensions/gii/generators/crud/templates/controller.php +++ b/extensions/gii/generators/crud/templates/controller.php @@ -14,7 +14,7 @@ $modelClass = StringHelper::basename($generator->modelClass); $searchModelClass = StringHelper::basename($generator->searchModelClass); if ($modelClass === $searchModelClass) { - $searchModelAlias = $searchModelClass . 'Search'; + $searchModelAlias = $searchModelClass . 'Search'; } /** @var ActiveRecordInterface $class */ @@ -41,121 +41,122 @@ */ class extends baseControllerClass) . "\n" ?> { - public function behaviors() - { - return [ - 'verbs' => [ - 'class' => VerbFilter::className(), - 'actions' => [ - 'delete' => ['post'], - ], - ], - ]; - } - - /** - * Lists all models. - * @return mixed - */ - public function actionIndex() - { - $searchModel = new ; - $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams()); - - return $this->render('index', [ - 'dataProvider' => $dataProvider, - 'searchModel' => $searchModel, - ]); - } - - /** - * Displays a single model. - * - * @return mixed - */ - public function actionView() - { - return $this->render('view', [ - 'model' => $this->findModel(), - ]); - } - - /** - * Creates a new model. - * If creation is successful, the browser will be redirected to the 'view' page. - * @return mixed - */ - public function actionCreate() - { - $model = new ; - - if ($model->load(Yii::$app->request->post()) && $model->save()) { - return $this->redirect(['view', ]); - } else { - return $this->render('create', [ - 'model' => $model, - ]); - } - } - - /** - * Updates an existing model. - * If update is successful, the browser will be redirected to the 'view' page. - * - * @return mixed - */ - public function actionUpdate() - { - $model = $this->findModel(); - - if ($model->load(Yii::$app->request->post()) && $model->save()) { - return $this->redirect(['view', ]); - } else { - return $this->render('update', [ - 'model' => $model, - ]); - } - } - - /** - * Deletes an existing model. - * If deletion is successful, the browser will be redirected to the 'index' page. - * - * @return mixed - */ - public function actionDelete() - { - $this->findModel()->delete(); - return $this->redirect(['index']); - } - - /** - * Finds the model based on its primary key value. - * If the model is not found, a 404 HTTP exception will be thrown. - * - * @return the loaded model - * @throws NotFoundHttpException if the model cannot be found - */ - protected function findModel() - { + public function behaviors() + { + return [ + 'verbs' => [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['post'], + ], + ], + ]; + } + + /** + * Lists all models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new ; + $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams()); + + return $this->render('index', [ + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel, + ]); + } + + /** + * Displays a single model. + * + * @return mixed + */ + public function actionView() + { + return $this->render('view', [ + 'model' => $this->findModel(), + ]); + } + + /** + * Creates a new model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new ; + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', ]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } + } + + /** + * Updates an existing model. + * If update is successful, the browser will be redirected to the 'view' page. + * + * @return mixed + */ + public function actionUpdate() + { + $model = $this->findModel(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', ]); + } else { + return $this->render('update', [ + 'model' => $model, + ]); + } + } + + /** + * Deletes an existing model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * + * @return mixed + */ + public function actionDelete() + { + $this->findModel()->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * + * @return the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel() + { \$$pk"; - } - $condition = '[' . implode(', ', $condition) . ']'; - $nullCheck = ''; + $condition = []; + foreach ($pks as $pk) { + $condition[] = "'$pk' => \$$pk"; + } + $condition = '[' . implode(', ', $condition) . ']'; + $nullCheck = ''; } ?> - if (($model = ::find()) !== null) { - return $model; - } else { - throw new NotFoundHttpException('The requested page does not exist.'); - } - } + if (($model = ::find()) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } } diff --git a/extensions/gii/generators/crud/templates/search.php b/extensions/gii/generators/crud/templates/search.php index 986250e9aab..8e8bdd1ea4f 100644 --- a/extensions/gii/generators/crud/templates/search.php +++ b/extensions/gii/generators/crud/templates/search.php @@ -12,7 +12,7 @@ $modelClass = StringHelper::basename($generator->modelClass); $searchModelClass = StringHelper::basename($generator->searchModelClass); if ($modelClass === $searchModelClass) { - $modelAlias = $modelClass . 'Model'; + $modelAlias = $modelClass . 'Model'; } $rules = $generator->generateSearchRules(); $labels = $generator->generateSearchLabels(); @@ -33,59 +33,59 @@ */ class extends Model { - public $; + public $; - public function rules() - { - return [ - , - ]; - } + public function rules() + { + return [ + , + ]; + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ $label): ?> - '" . addslashes($label) . "',\n" ?> + '" . addslashes($label) . "',\n" ?> - ]; - } + ]; + } - public function search($params) - { - $query = ::find(); - $dataProvider = new ActiveDataProvider([ - 'query' => $query, - ]); + public function search($params) + { + $query = ::find(); + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); - if (!($this->load($params) && $this->validate())) { - return $dataProvider; - } + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } - + - return $dataProvider; - } + return $dataProvider; + } - protected function addCondition($query, $attribute, $partialMatch = false) - { - if (($pos = strrpos($attribute, '.')) !== false) { - $modelAttribute = substr($attribute, $pos + 1); - } else { - $modelAttribute = $attribute; - } + protected function addCondition($query, $attribute, $partialMatch = false) + { + if (($pos = strrpos($attribute, '.')) !== false) { + $modelAttribute = substr($attribute, $pos + 1); + } else { + $modelAttribute = $attribute; + } - $value = $this->$modelAttribute; - if (trim($value) === '') { - return; - } - if ($partialMatch) { - $query->andWhere(['like', $attribute, $value]); - } else { - $query->andWhere([$attribute => $value]); - } - } + $value = $this->$modelAttribute; + if (trim($value) === '') { + return; + } + if ($partialMatch) { + $query->andWhere(['like', $attribute, $value]); + } else { + $query->andWhere([$attribute => $value]); + } + } } diff --git a/extensions/gii/generators/crud/templates/views/_form.php b/extensions/gii/generators/crud/templates/views/_form.php index 52538d50bcb..67136d8fd20 100644 --- a/extensions/gii/generators/crud/templates/views/_form.php +++ b/extensions/gii/generators/crud/templates/views/_form.php @@ -12,7 +12,7 @@ $model = new $generator->modelClass; $safeAttributes = $model->safeAttributes(); if (empty($safeAttributes)) { - $safeAttributes = $model->attributes(); + $safeAttributes = $model->attributes(); } echo "modelClass)) ?>-form"> - $form = ActiveForm::begin(); ?> + $form = ActiveForm::begin(); ?> generateActiveField($attribute) . " ?>\n\n"; + echo "\t\tgenerateActiveField($attribute) . " ?>\n\n"; } ?> -
    - Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> -
    +
    + Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
    - ActiveForm::end(); ?> + ActiveForm::end(); ?>
    diff --git a/extensions/gii/generators/crud/templates/views/_search.php b/extensions/gii/generators/crud/templates/views/_search.php index af2f9486e3c..e1921336aff 100644 --- a/extensions/gii/generators/crud/templates/views/_search.php +++ b/extensions/gii/generators/crud/templates/views/_search.php @@ -23,26 +23,26 @@ diff --git a/extensions/gii/generators/crud/templates/views/create.php b/extensions/gii/generators/crud/templates/views/create.php index 72a1f8dadc3..9dacc35a19b 100644 --- a/extensions/gii/generators/crud/templates/views/create.php +++ b/extensions/gii/generators/crud/templates/views/create.php @@ -24,10 +24,10 @@ ?>
    -

    Html::encode($this->title) ?>

    +

    Html::encode($this->title) ?>

    - $this->render('_form', [ - 'model' => $model, - ]) ?> + $this->render('_form', [ + 'model' => $model, + ]) ?>
    diff --git a/extensions/gii/generators/crud/templates/views/index.php b/extensions/gii/generators/crud/templates/views/index.php index aadc5865ac8..9c94a6e760d 100644 --- a/extensions/gii/generators/crud/templates/views/index.php +++ b/extensions/gii/generators/crud/templates/views/index.php @@ -28,54 +28,54 @@ ?>
    -

    Html::encode($this->title) ?>

    +

    Html::encode($this->title) ?>

    - indexWidgetType === 'grid' ? "// " : "") ?>echo $this->render('_search', ['model' => $searchModel]); ?> + indexWidgetType === 'grid' ? "// " : "") ?>echo $this->render('_search', ['model' => $searchModel]); ?> -

    - Html::a('Create modelClass)) ?>', ['create'], ['class' => 'btn btn-success']) ?> -

    +

    + Html::a('Create modelClass)) ?>', ['create'], ['class' => 'btn btn-success']) ?> +

    indexWidgetType === 'grid'): ?> - GridView::widget([ - 'dataProvider' => $dataProvider, - 'filterModel' => $searchModel, - 'columns' => [ - ['class' => 'yii\grid\SerialColumn'], + GridView::widget([ + 'dataProvider' => $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], getTableSchema()) === false) { - foreach ($generator->getColumnNames() as $name) { - if (++$count < 6) { - echo "\t\t\t'" . $name . "',\n"; - } else { - echo "\t\t\t// '" . $name . "',\n"; - } - } + foreach ($generator->getColumnNames() as $name) { + if (++$count < 6) { + echo "\t\t\t'" . $name . "',\n"; + } else { + echo "\t\t\t// '" . $name . "',\n"; + } + } } else { - foreach ($tableSchema->columns as $column) { - $format = $generator->generateColumnFormat($column); - if (++$count < 6) { - echo "\t\t\t'" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n"; - } else { - echo "\t\t\t// '" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n"; - } - } + foreach ($tableSchema->columns as $column) { + $format = $generator->generateColumnFormat($column); + if (++$count < 6) { + echo "\t\t\t'" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n"; + } else { + echo "\t\t\t// '" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n"; + } + } } ?> - ['class' => 'yii\grid\ActionColumn'], - ], - ]); ?> + ['class' => 'yii\grid\ActionColumn'], + ], + ]); ?> - ListView::widget([ - 'dataProvider' => $dataProvider, - 'itemOptions' => ['class' => 'item'], - 'itemView' => function ($model, $key, $index, $widget) { - return Html::a(Html::encode($model->), ['view', ]); - }, - ]) ?> + ListView::widget([ + 'dataProvider' => $dataProvider, + 'itemOptions' => ['class' => 'item'], + 'itemView' => function ($model, $key, $index, $widget) { + return Html::a(Html::encode($model->), ['view', ]); + }, + ]) ?>
    diff --git a/extensions/gii/generators/crud/templates/views/update.php b/extensions/gii/generators/crud/templates/views/update.php index 610b5bbdde0..78112066b39 100644 --- a/extensions/gii/generators/crud/templates/views/update.php +++ b/extensions/gii/generators/crud/templates/views/update.php @@ -27,10 +27,10 @@ ?>
    -

    Html::encode($this->title) ?>

    +

    Html::encode($this->title) ?>

    - $this->render('_form', [ - 'model' => $model, - ]) ?> + $this->render('_form', [ + 'model' => $model, + ]) ?>
    diff --git a/extensions/gii/generators/crud/templates/views/view.php b/extensions/gii/generators/crud/templates/views/view.php index 951fd46336f..57074e4ff81 100644 --- a/extensions/gii/generators/crud/templates/views/view.php +++ b/extensions/gii/generators/crud/templates/views/view.php @@ -27,35 +27,35 @@ ?>
    -

    Html::encode($this->title) ?>

    - -

    - Html::a('Update', ['update', ], ['class' => 'btn btn-primary']) ?> - Html::a('Delete', ['delete', ], [ - 'class' => 'btn btn-danger', - 'data' => [ - 'confirm' => Yii::t('app', 'Are you sure you want to delete this item?'), - 'method' => 'post', - ], - ]) ?> -

    - - DetailView::widget([ - 'model' => $model, - 'attributes' => [ +

    Html::encode($this->title) ?>

    + +

    + Html::a('Update', ['update', ], ['class' => 'btn btn-primary']) ?> + Html::a('Delete', ['delete', ], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('app', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> +

    + + DetailView::widget([ + 'model' => $model, + 'attributes' => [ getTableSchema()) === false) { - foreach ($generator->getColumnNames() as $name) { - echo "\t\t\t'" . $name . "',\n"; - } + foreach ($generator->getColumnNames() as $name) { + echo "\t\t\t'" . $name . "',\n"; + } } else { - foreach ($generator->getTableSchema()->columns as $column) { - $format = $generator->generateColumnFormat($column); - echo "\t\t\t'" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n"; - } + foreach ($generator->getTableSchema()->columns as $column) { + $format = $generator->generateColumnFormat($column); + echo "\t\t\t'" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n"; + } } ?> - ], - ]) ?> + ], + ]) ?>
    diff --git a/extensions/gii/generators/extension/Generator.php b/extensions/gii/generators/extension/Generator.php index be47fbc9a1d..f6a1c9d5a67 100644 --- a/extensions/gii/generators/extension/Generator.php +++ b/extensions/gii/generators/extension/Generator.php @@ -22,124 +22,124 @@ */ class Generator extends \yii\gii\Generator { - public $vendorName; - public $packageName = "yii2-"; - public $namespace; - public $type = "yii2-extension"; - public $keywords = "yii2,extension"; - public $title; - public $description; - public $outputPath = "@app/runtime/tmp-extensions"; - public $license; - public $authorName; - public $authorEmail; + public $vendorName; + public $packageName = "yii2-"; + public $namespace; + public $type = "yii2-extension"; + public $keywords = "yii2,extension"; + public $title; + public $description; + public $outputPath = "@app/runtime/tmp-extensions"; + public $license; + public $authorName; + public $authorEmail; - /** - * @inheritdoc - */ - public function getName() - { - return 'Extension Generator'; - } + /** + * @inheritdoc + */ + public function getName() + { + return 'Extension Generator'; + } - /** - * @inheritdoc - */ - public function getDescription() - { - return 'This generator helps you to generate the files needed by a Yii extension.'; - } + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator helps you to generate the files needed by a Yii extension.'; + } - /** - * @inheritdoc - */ - public function rules() - { - return array_merge( - parent::rules(), - [ - [['vendorName', 'packageName'], 'filter', 'filter' => 'trim'], - [ - [ - 'vendorName', - 'packageName', - 'namespace', - 'type', - 'license', - 'title', - 'description', - 'authorName', - 'authorEmail', - 'outputPath' - ], - 'required' - ], - [['keywords'], 'safe'], - [['authorEmail'], 'email'], - [ - ['vendorName', 'packageName'], - 'match', - 'pattern' => '/^[a-z0-9\-\.]+$/', - 'message' => 'Only lowercase word characters, dashes and dots are allowed.' - ], - [ - ['namespace'], - 'match', - 'pattern' => '/^[a-zA-Z0-9\\\]+\\\$/', - 'message' => 'Only letters, numbers and backslashes are allowed. PSR-4 namespaces must end with a namespace separator.' - ], - ] - ); - } + /** + * @inheritdoc + */ + public function rules() + { + return array_merge( + parent::rules(), + [ + [['vendorName', 'packageName'], 'filter', 'filter' => 'trim'], + [ + [ + 'vendorName', + 'packageName', + 'namespace', + 'type', + 'license', + 'title', + 'description', + 'authorName', + 'authorEmail', + 'outputPath' + ], + 'required' + ], + [['keywords'], 'safe'], + [['authorEmail'], 'email'], + [ + ['vendorName', 'packageName'], + 'match', + 'pattern' => '/^[a-z0-9\-\.]+$/', + 'message' => 'Only lowercase word characters, dashes and dots are allowed.' + ], + [ + ['namespace'], + 'match', + 'pattern' => '/^[a-zA-Z0-9\\\]+\\\$/', + 'message' => 'Only letters, numbers and backslashes are allowed. PSR-4 namespaces must end with a namespace separator.' + ], + ] + ); + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'vendorName' => 'Vendor Name', - 'packageName' => 'Package Name', - 'license' => 'License', - ]; - } + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'vendorName' => 'Vendor Name', + 'packageName' => 'Package Name', + 'license' => 'License', + ]; + } - /** - * @inheritdoc - */ - public function hints() - { - return [ - 'vendorName' => 'This refers to the name of the publisher, your GitHub user name is usually a good choice, eg. myself.', - 'packageName' => 'This is the name of the extension on packagist, eg. yii2-foobar.', - 'namespace' => 'PSR-4, eg. myself\foobar\ This will be added to your autoloading by composer. Do not use yii or yii2 in the namespace.', - 'keywords' => 'Comma separated keywords for this extension.', - 'outputPath' => 'The temporary location of the generated files.', - 'title' => 'A more descriptive name of your application for the README file.', - 'description' => 'A sentence or subline describing the main purpose of the extension.', - ]; - } + /** + * @inheritdoc + */ + public function hints() + { + return [ + 'vendorName' => 'This refers to the name of the publisher, your GitHub user name is usually a good choice, eg. myself.', + 'packageName' => 'This is the name of the extension on packagist, eg. yii2-foobar.', + 'namespace' => 'PSR-4, eg. myself\foobar\ This will be added to your autoloading by composer. Do not use yii or yii2 in the namespace.', + 'keywords' => 'Comma separated keywords for this extension.', + 'outputPath' => 'The temporary location of the generated files.', + 'title' => 'A more descriptive name of your application for the README file.', + 'description' => 'A sentence or subline describing the main purpose of the extension.', + ]; + } - /** - * @inheritdoc - */ - public function stickyAttributes() - { - return ['vendorName', 'outputPath', 'authorName', 'authorEmail']; - } + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return ['vendorName', 'outputPath', 'authorName', 'authorEmail']; + } - /** - * @inheritdoc - */ - public function successMessage() - { - $outputPath = realpath(\Yii::getAlias($this->outputPath)); - $output1 = <<outputPath)); + $output1 = <<The extension has been generated successfully.

    To enable it in your application, you need to create a git repository and require it via composer.

    EOD; - $code1 = <<packageName} git init @@ -148,121 +148,125 @@ public function successMessage() git remote add origin https://path.to/your/repo git push -u origin master EOD; - $output2 = <<The next step is just for initial development, skip it if you directly publish the extension on packagist.org

    Add the newly created repo to your composer.json.

    EOD; - $code2 = <<Note: You may use the url file://{$outputPath}/{$this->packageName} for testing.

    Require the package with composer

    EOD; - $code3 = <<vendorName}/{$this->packageName}:dev-master EOD; - $output4 = <<And use it in your application.

    EOD; - $code4 = <<namespace}AutoloadExample::widget(); EOD; - $output5 = <<When you have finished development register your extension at packagist.org.

    EOD; - $return = $output1 . '
    ' . highlight_string($code1, true) . '
    '; - $return .= $output2 . '
    ' . highlight_string($code2, true) . '
    '; - $return .= $output3 . '
    ' . highlight_string($code3, true) . '
    '; - $return .= $output4 . '
    ' . highlight_string($code4, true) . '
    '; - $return .= $output5; - return $return; - } + $return = $output1 . '
    ' . highlight_string($code1, true) . '
    '; + $return .= $output2 . '
    ' . highlight_string($code2, true) . '
    '; + $return .= $output3 . '
    ' . highlight_string($code3, true) . '
    '; + $return .= $output4 . '
    ' . highlight_string($code4, true) . '
    '; + $return .= $output5; - /** - * @inheritdoc - */ - public function requiredTemplates() - { - return ['composer.json', 'AutoloadExample.php', 'README.md']; - } + return $return; + } - /** - * @inheritdoc - */ - public function generate() - { - $files = []; - $modulePath = $this->getOutputPath(); - $files[] = new CodeFile( - $modulePath . '/' . $this->packageName . '/composer.json', - $this->render("composer.json") - ); - $files[] = new CodeFile( - $modulePath . '/' . $this->packageName . '/AutoloadExample.php', - $this->render("AutoloadExample.php") - ); - $files[] = new CodeFile( - $modulePath . '/' . $this->packageName . '/README.md', - $this->render("README.md") - ); - return $files; - } + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return ['composer.json', 'AutoloadExample.php', 'README.md']; + } - /** - * @return boolean the directory that contains the module class - */ - public function getOutputPath() - { - return Yii::getAlias($this->outputPath); - } + /** + * @inheritdoc + */ + public function generate() + { + $files = []; + $modulePath = $this->getOutputPath(); + $files[] = new CodeFile( + $modulePath . '/' . $this->packageName . '/composer.json', + $this->render("composer.json") + ); + $files[] = new CodeFile( + $modulePath . '/' . $this->packageName . '/AutoloadExample.php', + $this->render("AutoloadExample.php") + ); + $files[] = new CodeFile( + $modulePath . '/' . $this->packageName . '/README.md', + $this->render("README.md") + ); - /** - * @return string a json encoded array with the given keywords - */ - public function getKeywordsArrayJson() - { - return json_encode(explode(',', $this->keywords)); - } + return $files; + } - /** - * @return array options for type drop-down - */ - public function optsType() - { - $licenses = [ - 'yii2-extension', - 'library', - ]; - return array_combine($licenses, $licenses); - } + /** + * @return boolean the directory that contains the module class + */ + public function getOutputPath() + { + return Yii::getAlias($this->outputPath); + } - /** - * @return array options for license drop-down - */ - public function optsLicense() - { - $licenses = [ - 'Apache-2.0', - 'BSD-2-Clause', - 'BSD-3-Clause', - 'BSD-4-Clause', - 'GPL-2.0', - 'GPL-2.0+', - 'GPL-3.0', - 'GPL-3.0+', - 'LGPL-2.1', - 'LGPL-2.1+', - 'LGPL-3.0', - 'LGPL-3.0+', - 'MIT' - ]; - return array_combine($licenses, $licenses); - } + /** + * @return string a json encoded array with the given keywords + */ + public function getKeywordsArrayJson() + { + return json_encode(explode(',', $this->keywords)); + } + + /** + * @return array options for type drop-down + */ + public function optsType() + { + $licenses = [ + 'yii2-extension', + 'library', + ]; + + return array_combine($licenses, $licenses); + } + + /** + * @return array options for license drop-down + */ + public function optsLicense() + { + $licenses = [ + 'Apache-2.0', + 'BSD-2-Clause', + 'BSD-3-Clause', + 'BSD-4-Clause', + 'GPL-2.0', + 'GPL-2.0+', + 'GPL-3.0', + 'GPL-3.0+', + 'LGPL-2.1', + 'LGPL-2.1+', + 'LGPL-3.0', + 'LGPL-3.0+', + 'MIT' + ]; + + return array_combine($licenses, $licenses); + } } diff --git a/extensions/gii/generators/extension/form.php b/extensions/gii/generators/extension/form.php index 98a250cafc1..b5b1ac9fc79 100644 --- a/extensions/gii/generators/extension/form.php +++ b/extensions/gii/generators/extension/form.php @@ -6,22 +6,22 @@ */ ?>
    - Please read the + Please read the 'new']) ?> - before creating an extension. + before creating an extension.
    field($generator, 'vendorName'); - echo $form->field($generator, 'packageName'); - echo $form->field($generator, 'namespace'); - echo $form->field($generator, 'type')->dropDownList($generator->optsType()); - echo $form->field($generator, 'keywords'); - echo $form->field($generator, 'license')->dropDownList($generator->optsLicense(), ['prompt'=>'Choose...']); - echo $form->field($generator, 'title'); - echo $form->field($generator, 'description'); - echo $form->field($generator, 'authorName'); - echo $form->field($generator, 'authorEmail'); - echo $form->field($generator, 'outputPath'); + echo $form->field($generator, 'vendorName'); + echo $form->field($generator, 'packageName'); + echo $form->field($generator, 'namespace'); + echo $form->field($generator, 'type')->dropDownList($generator->optsType()); + echo $form->field($generator, 'keywords'); + echo $form->field($generator, 'license')->dropDownList($generator->optsLicense(), ['prompt'=>'Choose...']); + echo $form->field($generator, 'title'); + echo $form->field($generator, 'description'); + echo $form->field($generator, 'authorName'); + echo $form->field($generator, 'authorEmail'); + echo $form->field($generator, 'outputPath'); ?>
    diff --git a/extensions/gii/generators/extension/templates/AutoloadExample.php b/extensions/gii/generators/extension/templates/AutoloadExample.php index 194ba720f69..96ff3de702d 100644 --- a/extensions/gii/generators/extension/templates/AutoloadExample.php +++ b/extensions/gii/generators/extension/templates/AutoloadExample.php @@ -7,8 +7,10 @@ namespace namespace, 0, -1) ?>; -class AutoloadExample extends \yii\base\widget { - function run() { +class AutoloadExample extends \yii\base\widget +{ + public function run() + { return "Hello!"; } } diff --git a/extensions/gii/generators/form/Generator.php b/extensions/gii/generators/form/Generator.php index 9140d71d6b9..fe29d294809 100644 --- a/extensions/gii/generators/form/Generator.php +++ b/extensions/gii/generators/form/Generator.php @@ -21,134 +21,136 @@ */ class Generator extends \yii\gii\Generator { - public $modelClass; - public $viewPath = '@app/views'; - public $viewName; - public $scenarioName; - - - /** - * @inheritdoc - */ - public function getName() - { - return 'Form Generator'; - } - - /** - * @inheritdoc - */ - public function getDescription() - { - return 'This generator generates a view script file that displays a form to collect input for the specified model class.'; - } - - /** - * @inheritdoc - */ - public function generate() - { - $files = []; - $files[] = new CodeFile( - Yii::getAlias($this->viewPath) . '/' . $this->viewName . '.php', - $this->render('form.php') - ); - return $files; - } - - /** - * @inheritdoc - */ - public function rules() - { - return array_merge(parent::rules(), [ - [['modelClass', 'viewName', 'scenarioName', 'viewPath'], 'filter', 'filter' => 'trim'], - [['modelClass', 'viewName', 'viewPath'], 'required'], - [['modelClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], - [['modelClass'], 'validateClass', 'params' => ['extends' => Model::className()]], - [['viewName'], 'match', 'pattern' => '/^\w+[\\-\\/\w]*$/', 'message' => 'Only word characters, dashes and slashes are allowed.'], - [['viewPath'], 'match', 'pattern' => '/^@?\w+[\\-\\/\w]*$/', 'message' => 'Only word characters, dashes, slashes and @ are allowed.'], - [['viewPath'], 'validateViewPath'], - [['scenarioName'], 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'], - ]); - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'modelClass' => 'Model Class', - 'viewName' => 'View Name', - 'viewPath' => 'View Path', - 'scenarioName' => 'Scenario', - ]; - } - - /** - * @inheritdoc - */ - public function requiredTemplates() - { - return ['form.php', 'action.php']; - } - - /** - * @inheritdoc - */ - public function stickyAttributes() - { - return ['viewPath', 'scenarioName']; - } - - /** - * @inheritdoc - */ - public function hints() - { - return [ - 'modelClass' => 'This is the model class for collecting the form input. You should provide a fully qualified class name, e.g., app\models\Post.', - 'viewName' => 'This is the view name with respect to the view path. For example, site/index would generate a site/index.php view file under the view path.', - 'viewPath' => 'This is the root view path to keep the generated view files. You may provide either a directory or a path alias, e.g., @app/views.', - 'scenarioName' => 'This is the scenario to be used by the model when collecting the form input. If empty, the default scenario will be used.', - ]; - } - - /** - * @inheritdoc - */ - public function successMessage() - { - $code = highlight_string($this->render('action.php'), true); - return <<viewPath) . '/' . $this->viewName . '.php', + $this->render('form.php') + ); + + return $files; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), [ + [['modelClass', 'viewName', 'scenarioName', 'viewPath'], 'filter', 'filter' => 'trim'], + [['modelClass', 'viewName', 'viewPath'], 'required'], + [['modelClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], + [['modelClass'], 'validateClass', 'params' => ['extends' => Model::className()]], + [['viewName'], 'match', 'pattern' => '/^\w+[\\-\\/\w]*$/', 'message' => 'Only word characters, dashes and slashes are allowed.'], + [['viewPath'], 'match', 'pattern' => '/^@?\w+[\\-\\/\w]*$/', 'message' => 'Only word characters, dashes, slashes and @ are allowed.'], + [['viewPath'], 'validateViewPath'], + [['scenarioName'], 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'], + ]); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'modelClass' => 'Model Class', + 'viewName' => 'View Name', + 'viewPath' => 'View Path', + 'scenarioName' => 'Scenario', + ]; + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return ['form.php', 'action.php']; + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return ['viewPath', 'scenarioName']; + } + + /** + * @inheritdoc + */ + public function hints() + { + return [ + 'modelClass' => 'This is the model class for collecting the form input. You should provide a fully qualified class name, e.g., app\models\Post.', + 'viewName' => 'This is the view name with respect to the view path. For example, site/index would generate a site/index.php view file under the view path.', + 'viewPath' => 'This is the root view path to keep the generated view files. You may provide either a directory or a path alias, e.g., @app/views.', + 'scenarioName' => 'This is the scenario to be used by the model when collecting the form input. If empty, the default scenario will be used.', + ]; + } + + /** + * @inheritdoc + */ + public function successMessage() + { + $code = highlight_string($this->render('action.php'), true); + + return <<The form has been generated successfully.

    You may add the following code in an appropriate controller class to invoke the view:

    $code
    EOD; - } - - /** - * Validates [[viewPath]] to make sure it is a valid path or path alias and exists. - */ - public function validateViewPath() - { - $path = Yii::getAlias($this->viewPath, false); - if ($path === false || !is_dir($path)) { - $this->addError('viewPath', 'View path does not exist.'); - } - } - - /** - * @return array list of safe attributes of [[modelClass]] - */ - public function getModelAttributes() - { - /** @var Model $model */ - $model = new $this->modelClass; - if (!empty($this->scenarioName)) { - $model->setScenario($this->scenarioName); - } - return $model->safeAttributes(); - } + } + + /** + * Validates [[viewPath]] to make sure it is a valid path or path alias and exists. + */ + public function validateViewPath() + { + $path = Yii::getAlias($this->viewPath, false); + if ($path === false || !is_dir($path)) { + $this->addError('viewPath', 'View path does not exist.'); + } + } + + /** + * @return array list of safe attributes of [[modelClass]] + */ + public function getModelAttributes() + { + /** @var Model $model */ + $model = new $this->modelClass; + if (!empty($this->scenarioName)) { + $model->setScenario($this->scenarioName); + } + + return $model->safeAttributes(); + } } diff --git a/extensions/gii/generators/form/templates/action.php b/extensions/gii/generators/form/templates/action.php index c7e1799548e..f9587bcdaf3 100644 --- a/extensions/gii/generators/form/templates/action.php +++ b/extensions/gii/generators/form/templates/action.php @@ -14,15 +14,16 @@ public function actionviewName), '_')) ?>() { - $model = new modelClass ?>scenarioName) ? "" : "(['scenario' => '{$generator->scenarioName}'])" ?>; + $model = new modelClass ?>scenarioName) ? "" : "(['scenario' => '{$generator->scenarioName}'])" ?>; - if ($model->load(Yii::$app->request->post())) { - if ($model->validate()) { - // form inputs are valid, do something here - return; - } - } - return $this->render('viewName ?>', [ - 'model' => $model, - ]); + if ($model->load(Yii::$app->request->post())) { + if ($model->validate()) { + // form inputs are valid, do something here + return; + } + } + + return $this->render('viewName ?>', [ + 'model' => $model, + ]); } diff --git a/extensions/gii/generators/form/templates/form.php b/extensions/gii/generators/form/templates/form.php index b30570c0df3..97b0d79b977 100644 --- a/extensions/gii/generators/form/templates/form.php +++ b/extensions/gii/generators/form/templates/form.php @@ -21,15 +21,15 @@
    - $form = ActiveForm::begin(); ?> + $form = ActiveForm::begin(); ?> - getModelAttributes() as $attribute): ?> - $form->field($model, '') ?> - + getModelAttributes() as $attribute): ?> + $form->field($model, '') ?> + -
    - Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?> -
    - ActiveForm::end(); ?> +
    + Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?> +
    + ActiveForm::end(); ?>
    diff --git a/extensions/gii/generators/model/Generator.php b/extensions/gii/generators/model/Generator.php index 893cd7facc6..7f030f1475a 100644 --- a/extensions/gii/generators/model/Generator.php +++ b/extensions/gii/generators/model/Generator.php @@ -23,566 +23,573 @@ */ class Generator extends \yii\gii\Generator { - public $db = 'db'; - public $ns = 'app\models'; - public $tableName; - public $modelClass; - public $baseClass = 'yii\db\ActiveRecord'; - public $generateRelations = true; - public $generateLabelsFromComments = false; - - - /** - * @inheritdoc - */ - public function getName() - { - return 'Model Generator'; - } - - /** - * @inheritdoc - */ - public function getDescription() - { - return 'This generator generates an ActiveRecord class for the specified database table.'; - } - - /** - * @inheritdoc - */ - public function rules() - { - return array_merge(parent::rules(), [ - [['db', 'ns', 'tableName', 'modelClass', 'baseClass'], 'filter', 'filter' => 'trim'], - [['db', 'ns', 'tableName', 'baseClass'], 'required'], - [['db', 'modelClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'], - [['ns', 'baseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'], - [['tableName'], 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'], - [['db'], 'validateDb'], - [['ns'], 'validateNamespace'], - [['tableName'], 'validateTableName'], - [['modelClass'], 'validateModelClass', 'skipOnEmpty' => false], - [['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]], - [['generateRelations', 'generateLabelsFromComments'], 'boolean'], - ]); - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'ns' => 'Namespace', - 'db' => 'Database Connection ID', - 'tableName' => 'Table Name', - 'modelClass' => 'Model Class', - 'baseClass' => 'Base Class', - 'generateRelations' => 'Generate Relations', - 'generateLabelsFromComments' => 'Generate Labels from DB Comments', - ]; - } - - /** - * @inheritdoc - */ - public function hints() - { - return [ - 'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., app\models', - 'db' => 'This is the ID of the DB application component.', - 'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. tbl_post. - The table name may consist of the DB schema part if needed, e.g. public.tbl_post. - The table name may end with asterisk to match multiple table names, e.g. tbl_* - will match tables who name starts with tbl_. In this case, multiple ActiveRecord classes - will be generated, one for each matching table name; and the class names will be generated from - the matching characters. For example, table tbl_post will generate Post - class.', - 'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain - the namespace part as it is specified in "Namespace". You do not need to specify the class name - if "Table Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.', - 'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.', - 'generateRelations' => 'This indicates whether the generator should generate relations based on - foreign key constraints it detects in the database. Note that if your database contains too many tables, - you may want to uncheck this option to accelerate the code generation process.', - 'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels - by using the comments of the corresponding DB columns.', - ]; - } - - /** - * @inheritdoc - */ - public function autoCompleteData() - { - $db = $this->getDbConnection(); - if ($db !== null) { - return [ - 'tableName' => function () use ($db) { - return $db->getSchema()->getTableNames(); - }, - ]; - } else { - return []; - } - } - - /** - * @inheritdoc - */ - public function requiredTemplates() - { - return ['model.php']; - } - - /** - * @inheritdoc - */ - public function stickyAttributes() - { - return ['ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments']; - } - - /** - * @inheritdoc - */ - public function generate() - { - $files = []; - $relations = $this->generateRelations(); - $db = $this->getDbConnection(); - foreach ($this->getTableNames() as $tableName) { - $className = $this->generateClassName($tableName); - $tableSchema = $db->getTableSchema($tableName); - $params = [ - 'tableName' => $tableName, - 'className' => $className, - 'tableSchema' => $tableSchema, - 'labels' => $this->generateLabels($tableSchema), - 'rules' => $this->generateRules($tableSchema), - 'relations' => isset($relations[$className]) ? $relations[$className] : [], - ]; - $files[] = new CodeFile( - Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php', - $this->render('model.php', $params) - ); - } - - return $files; - } - - /** - * Generates the attribute labels for the specified table. - * @param \yii\db\TableSchema $table the table schema - * @return array the generated attribute labels (name => label) - */ - public function generateLabels($table) - { - $labels = []; - foreach ($table->columns as $column) { - if ($this->generateLabelsFromComments && !empty($column->comment)) { - $labels[$column->name] = $column->comment; - } elseif (!strcasecmp($column->name, 'id')) { - $labels[$column->name] = 'ID'; - } else { - $label = Inflector::camel2words($column->name); - if (strcasecmp(substr($label, -3), ' id') === 0) { - $label = substr($label, 0, -3) . ' ID'; - } - $labels[$column->name] = $label; - } - } - return $labels; - } - - /** - * Generates validation rules for the specified table. - * @param \yii\db\TableSchema $table the table schema - * @return array the generated validation rules - */ - public function generateRules($table) - { - $types = []; - $lengths = []; - foreach ($table->columns as $column) { - if ($column->autoIncrement) { - continue; - } - if (!$column->allowNull && $column->defaultValue === null) { - $types['required'][] = $column->name; - } - switch ($column->type) { - case Schema::TYPE_SMALLINT: - case Schema::TYPE_INTEGER: - case Schema::TYPE_BIGINT: - $types['integer'][] = $column->name; - break; - case Schema::TYPE_BOOLEAN: - $types['boolean'][] = $column->name; - break; - case Schema::TYPE_FLOAT: - case Schema::TYPE_DECIMAL: - case Schema::TYPE_MONEY: - $types['number'][] = $column->name; - break; - case Schema::TYPE_DATE: - case Schema::TYPE_TIME: - case Schema::TYPE_DATETIME: - case Schema::TYPE_TIMESTAMP: - $types['safe'][] = $column->name; - break; - default: // strings - if ($column->size > 0) { - $lengths[$column->size][] = $column->name; - } else { - $types['string'][] = $column->name; - } - } - } - $rules = []; - foreach ($types as $type => $columns) { - $rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; - } - foreach ($lengths as $length => $columns) { - $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]"; - } - - // Unique indexes rules - try { - $db = $this->getDbConnection(); - $uniqueIndexes = $db->getSchema()->findUniqueIndexes($table); - foreach ($uniqueIndexes as $uniqueColumns) { - // Avoid validating auto incrementable columns - if (!$this->isUniqueColumnAutoIncrementable($table, $uniqueColumns)) { - $attributesCount = count($uniqueColumns); - - if ($attributesCount == 1) { - $rules[] = "[['" . $uniqueColumns[0] . "'], 'unique']"; - } elseif ($attributesCount > 1) { - $labels = array_intersect_key($this->generateLabels($table), array_flip($uniqueColumns)); - $lastLabel = array_pop($labels); - $columnsList = implode("', '", $uniqueColumns); - $rules[] = "[['" . $columnsList . "'], 'unique', 'targetAttribute' => ['" . $columnsList . "'], 'message' => 'The combination of " . implode(', ', $labels) . " and " . $lastLabel . " has already been taken.']"; - } - } - } - } catch (NotSupportedException $e) { - // doesn't support unique indexes information...do nothing - } - return $rules; - } - - /** - * @return array the generated relation declarations - */ - protected function generateRelations() - { - if (!$this->generateRelations) { - return []; - } - - $db = $this->getDbConnection(); - - if (($pos = strpos($this->tableName, '.')) !== false) { - $schemaName = substr($this->tableName, 0, $pos); - } else { - $schemaName = ''; - } - - $relations = []; - foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) { - $tableName = $table->name; - $className = $this->generateClassName($tableName); - foreach ($table->foreignKeys as $refs) { - $refTable = $refs[0]; - unset($refs[0]); - $fks = array_keys($refs); - $refClassName = $this->generateClassName($refTable); - - // Add relation for this table - $link = $this->generateRelationLink(array_flip($refs)); - $relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false); - $relations[$className][$relationName] = [ - "return \$this->hasOne($refClassName::className(), $link);", - $refClassName, - false, - ]; - - // Add relation for the referenced table - $hasMany = false; - foreach ($fks as $key) { - if (!in_array($key, $table->primaryKey, true)) { - $hasMany = true; - break; - } - } - $link = $this->generateRelationLink($refs); - $relationName = $this->generateRelationName($relations, $refClassName, $refTable, $className, $hasMany); - $relations[$refClassName][$relationName] = [ - "return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "($className::className(), $link);", - $className, - $hasMany, - ]; - } - - if (($fks = $this->checkPivotTable($table)) === false) { - continue; - } - $table0 = $fks[$table->primaryKey[0]][0]; - $table1 = $fks[$table->primaryKey[1]][0]; - $className0 = $this->generateClassName($table0); - $className1 = $this->generateClassName($table1); - - $link = $this->generateRelationLink([$fks[$table->primaryKey[1]][1] => $table->primaryKey[1]]); - $viaLink = $this->generateRelationLink([$table->primaryKey[0] => $fks[$table->primaryKey[0]][1]]); - $relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true); - $relations[$className0][$relationName] = [ - "return \$this->hasMany($className1::className(), $link)->viaTable('{$table->name}', $viaLink);", - $className1, - true, - ]; - - $link = $this->generateRelationLink([$fks[$table->primaryKey[0]][1] => $table->primaryKey[0]]); - $viaLink = $this->generateRelationLink([$table->primaryKey[1] => $fks[$table->primaryKey[1]][1]]); - $relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true); - $relations[$className1][$relationName] = [ - "return \$this->hasMany($className0::className(), $link)->viaTable('{$table->name}', $viaLink);", - $className0, - true, - ]; - } - return $relations; - } - - /** - * Generates the link parameter to be used in generating the relation declaration. - * @param array $refs reference constraint - * @return string the generated link parameter. - */ - protected function generateRelationLink($refs) - { - $pairs = []; - foreach ($refs as $a => $b) { - $pairs[] = "'$a' => '$b'"; - } - return '[' . implode(', ', $pairs) . ']'; - } - - /** - * Checks if the given table is a pivot table. - * For simplicity, this method only deals with the case where the pivot contains two PK columns, - * each referencing a column in a different table. - * @param \yii\db\TableSchema the table being checked - * @return array|boolean the relevant foreign key constraint information if the table is a pivot table, - * or false if the table is not a pivot table. - */ - protected function checkPivotTable($table) - { - $pk = $table->primaryKey; - if (count($pk) !== 2) { - return false; - } - $fks = []; - foreach ($table->foreignKeys as $refs) { - if (count($refs) === 2) { - if (isset($refs[$pk[0]])) { - $fks[$pk[0]] = [$refs[0], $refs[$pk[0]]]; - } elseif (isset($refs[$pk[1]])) { - $fks[$pk[1]] = [$refs[0], $refs[$pk[1]]]; - } - } - } - if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) { - return $fks; - } else { - return false; - } - } - - /** - * Generate a relation name for the specified table and a base name. - * @param array $relations the relations being generated currently. - * @param string $className the class name that will contain the relation declarations - * @param \yii\db\TableSchema $table the table schema - * @param string $key a base name that the relation name may be generated from - * @param boolean $multiple whether this is a has-many relation - * @return string the relation name - */ - protected function generateRelationName($relations, $className, $table, $key, $multiple) - { - if (strcasecmp(substr($key, -2), 'id') === 0 && strcasecmp($key, 'id')) { - $key = rtrim(substr($key, 0, -2), '_'); - } - if ($multiple) { - $key = Inflector::pluralize($key); - } - $name = $rawName = Inflector::id2camel($key, '_'); - $i = 0; - while (isset($table->columns[lcfirst($name)])) { - $name = $rawName . ($i++); - } - while (isset($relations[$className][lcfirst($name)])) { - $name = $rawName . ($i++); - } - - return $name; - } - - /** - * Validates the [[db]] attribute. - */ - public function validateDb() - { - if (Yii::$app->hasComponent($this->db) === false) { - $this->addError('db', 'There is no application component named "db".'); - } elseif (!Yii::$app->getComponent($this->db) instanceof Connection) { - $this->addError('db', 'The "db" application component must be a DB connection instance.'); - } - } - - /** - * Validates the [[ns]] attribute. - */ - public function validateNamespace() - { - $this->ns = ltrim($this->ns, '\\'); - $path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false); - if ($path === false) { - $this->addError('ns', 'Namespace must be associated with an existing directory.'); - } - } - - /** - * Validates the [[modelClass]] attribute. - */ - public function validateModelClass() - { - if ($this->isReservedKeyword($this->modelClass)) { - $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.'); - } - if (substr($this->tableName, -1) !== '*' && $this->modelClass == '') { - $this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.'); - } - } - - /** - * Validates the [[tableName]] attribute. - */ - public function validateTableName() - { - if (strpos($this->tableName, '*') !== false && substr($this->tableName, -1) !== '*') { - $this->addError('tableName', 'Asterisk is only allowed as the last character.'); - return; - } - $tables = $this->getTableNames(); - if (empty($tables)) { - $this->addError('tableName', "Table '{$this->tableName}' does not exist."); - } else { - foreach ($tables as $table) { - $class = $this->generateClassName($table); - if ($this->isReservedKeyword($class)) { - $this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword."); - break; - } - } - } - } - - private $_tableNames; - private $_classNames; - - /** - * @return array the table names that match the pattern specified by [[tableName]]. - */ - protected function getTableNames() - { - if ($this->_tableNames !== null) { - return $this->_tableNames; - } - $db = $this->getDbConnection(); - if ($db === null) { - return []; - } - $tableNames = []; - if (strpos($this->tableName, '*') !== false) { - if (($pos = strrpos($this->tableName, '.')) !== false) { - $schema = substr($this->tableName, 0, $pos); - $pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/'; - } else { - $schema = ''; - $pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/'; - } - - foreach ($db->schema->getTableNames($schema) as $table) { - if (preg_match($pattern, $table)) { - $tableNames[] = $schema === '' ? $table : ($schema . '.' . $table); - } - } - } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) { - $tableNames[] = $this->tableName; - $this->_classNames[$this->tableName] = $this->modelClass; - } - return $this->_tableNames = $tableNames; - } - - /** - * Generates a class name from the specified table name. - * @param string $tableName the table name (which may contain schema prefix) - * @return string the generated class name - */ - protected function generateClassName($tableName) - { - if (isset($this->_classNames[$tableName])) { - return $this->_classNames[$tableName]; - } - - if (($pos = strrpos($tableName, '.')) !== false) { - $tableName = substr($tableName, $pos + 1); - } - - $db = $this->getDbConnection(); - $patterns = []; - $patterns[] = "/^{$db->tablePrefix}(.*?)$/"; - $patterns[] = "/^(.*?){$db->tablePrefix}$/"; - if (strpos($this->tableName, '*') !== false) { - $pattern = $this->tableName; - if (($pos = strrpos($pattern, '.')) !== false) { - $pattern = substr($pattern, $pos + 1); - } - $patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/'; - } - $className = $tableName; - foreach ($patterns as $pattern) { - if (preg_match($pattern, $tableName, $matches)) { - $className = $matches[1]; - break; - } - } - return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); - } - - /** - * @return Connection the DB connection as specified by [[db]]. - */ - protected function getDbConnection() - { - return Yii::$app->{$this->db}; - } - - /** - * Checks if any of the specified columns of an unique index is auto incrementable. - * @param \yii\db\TableSchema $table the table schema - * @param array $columns columns to check for autoIncrement property - * @return boolean whether any of the specified columns is auto incrementable. - */ - protected function isUniqueColumnAutoIncrementable($table, $columns) - { - foreach ($columns as $column) { - if ($table->columns[$column]->autoIncrement) { - return true; - } - } - return false; - } + public $db = 'db'; + public $ns = 'app\models'; + public $tableName; + public $modelClass; + public $baseClass = 'yii\db\ActiveRecord'; + public $generateRelations = true; + public $generateLabelsFromComments = false; + + /** + * @inheritdoc + */ + public function getName() + { + return 'Model Generator'; + } + + /** + * @inheritdoc + */ + public function getDescription() + { + return 'This generator generates an ActiveRecord class for the specified database table.'; + } + + /** + * @inheritdoc + */ + public function rules() + { + return array_merge(parent::rules(), [ + [['db', 'ns', 'tableName', 'modelClass', 'baseClass'], 'filter', 'filter' => 'trim'], + [['db', 'ns', 'tableName', 'baseClass'], 'required'], + [['db', 'modelClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'], + [['ns', 'baseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'], + [['tableName'], 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'], + [['db'], 'validateDb'], + [['ns'], 'validateNamespace'], + [['tableName'], 'validateTableName'], + [['modelClass'], 'validateModelClass', 'skipOnEmpty' => false], + [['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]], + [['generateRelations', 'generateLabelsFromComments'], 'boolean'], + ]); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'ns' => 'Namespace', + 'db' => 'Database Connection ID', + 'tableName' => 'Table Name', + 'modelClass' => 'Model Class', + 'baseClass' => 'Base Class', + 'generateRelations' => 'Generate Relations', + 'generateLabelsFromComments' => 'Generate Labels from DB Comments', + ]; + } + + /** + * @inheritdoc + */ + public function hints() + { + return [ + 'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., app\models', + 'db' => 'This is the ID of the DB application component.', + 'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. tbl_post. + The table name may consist of the DB schema part if needed, e.g. public.tbl_post. + The table name may end with asterisk to match multiple table names, e.g. tbl_* + will match tables who name starts with tbl_. In this case, multiple ActiveRecord classes + will be generated, one for each matching table name; and the class names will be generated from + the matching characters. For example, table tbl_post will generate Post + class.', + 'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain + the namespace part as it is specified in "Namespace". You do not need to specify the class name + if "Table Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.', + 'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.', + 'generateRelations' => 'This indicates whether the generator should generate relations based on + foreign key constraints it detects in the database. Note that if your database contains too many tables, + you may want to uncheck this option to accelerate the code generation process.', + 'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels + by using the comments of the corresponding DB columns.', + ]; + } + + /** + * @inheritdoc + */ + public function autoCompleteData() + { + $db = $this->getDbConnection(); + if ($db !== null) { + return [ + 'tableName' => function () use ($db) { + return $db->getSchema()->getTableNames(); + }, + ]; + } else { + return []; + } + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return ['model.php']; + } + + /** + * @inheritdoc + */ + public function stickyAttributes() + { + return ['ns', 'db', 'baseClass', 'generateRelations', 'generateLabelsFromComments']; + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = []; + $relations = $this->generateRelations(); + $db = $this->getDbConnection(); + foreach ($this->getTableNames() as $tableName) { + $className = $this->generateClassName($tableName); + $tableSchema = $db->getTableSchema($tableName); + $params = [ + 'tableName' => $tableName, + 'className' => $className, + 'tableSchema' => $tableSchema, + 'labels' => $this->generateLabels($tableSchema), + 'rules' => $this->generateRules($tableSchema), + 'relations' => isset($relations[$className]) ? $relations[$className] : [], + ]; + $files[] = new CodeFile( + Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php', + $this->render('model.php', $params) + ); + } + + return $files; + } + + /** + * Generates the attribute labels for the specified table. + * @param \yii\db\TableSchema $table the table schema + * @return array the generated attribute labels (name => label) + */ + public function generateLabels($table) + { + $labels = []; + foreach ($table->columns as $column) { + if ($this->generateLabelsFromComments && !empty($column->comment)) { + $labels[$column->name] = $column->comment; + } elseif (!strcasecmp($column->name, 'id')) { + $labels[$column->name] = 'ID'; + } else { + $label = Inflector::camel2words($column->name); + if (strcasecmp(substr($label, -3), ' id') === 0) { + $label = substr($label, 0, -3) . ' ID'; + } + $labels[$column->name] = $label; + } + } + + return $labels; + } + + /** + * Generates validation rules for the specified table. + * @param \yii\db\TableSchema $table the table schema + * @return array the generated validation rules + */ + public function generateRules($table) + { + $types = []; + $lengths = []; + foreach ($table->columns as $column) { + if ($column->autoIncrement) { + continue; + } + if (!$column->allowNull && $column->defaultValue === null) { + $types['required'][] = $column->name; + } + switch ($column->type) { + case Schema::TYPE_SMALLINT: + case Schema::TYPE_INTEGER: + case Schema::TYPE_BIGINT: + $types['integer'][] = $column->name; + break; + case Schema::TYPE_BOOLEAN: + $types['boolean'][] = $column->name; + break; + case Schema::TYPE_FLOAT: + case Schema::TYPE_DECIMAL: + case Schema::TYPE_MONEY: + $types['number'][] = $column->name; + break; + case Schema::TYPE_DATE: + case Schema::TYPE_TIME: + case Schema::TYPE_DATETIME: + case Schema::TYPE_TIMESTAMP: + $types['safe'][] = $column->name; + break; + default: // strings + if ($column->size > 0) { + $lengths[$column->size][] = $column->name; + } else { + $types['string'][] = $column->name; + } + } + } + $rules = []; + foreach ($types as $type => $columns) { + $rules[] = "[['" . implode("', '", $columns) . "'], '$type']"; + } + foreach ($lengths as $length => $columns) { + $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]"; + } + + // Unique indexes rules + try { + $db = $this->getDbConnection(); + $uniqueIndexes = $db->getSchema()->findUniqueIndexes($table); + foreach ($uniqueIndexes as $uniqueColumns) { + // Avoid validating auto incrementable columns + if (!$this->isUniqueColumnAutoIncrementable($table, $uniqueColumns)) { + $attributesCount = count($uniqueColumns); + + if ($attributesCount == 1) { + $rules[] = "[['" . $uniqueColumns[0] . "'], 'unique']"; + } elseif ($attributesCount > 1) { + $labels = array_intersect_key($this->generateLabels($table), array_flip($uniqueColumns)); + $lastLabel = array_pop($labels); + $columnsList = implode("', '", $uniqueColumns); + $rules[] = "[['" . $columnsList . "'], 'unique', 'targetAttribute' => ['" . $columnsList . "'], 'message' => 'The combination of " . implode(', ', $labels) . " and " . $lastLabel . " has already been taken.']"; + } + } + } + } catch (NotSupportedException $e) { + // doesn't support unique indexes information...do nothing + } + + return $rules; + } + + /** + * @return array the generated relation declarations + */ + protected function generateRelations() + { + if (!$this->generateRelations) { + return []; + } + + $db = $this->getDbConnection(); + + if (($pos = strpos($this->tableName, '.')) !== false) { + $schemaName = substr($this->tableName, 0, $pos); + } else { + $schemaName = ''; + } + + $relations = []; + foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) { + $tableName = $table->name; + $className = $this->generateClassName($tableName); + foreach ($table->foreignKeys as $refs) { + $refTable = $refs[0]; + unset($refs[0]); + $fks = array_keys($refs); + $refClassName = $this->generateClassName($refTable); + + // Add relation for this table + $link = $this->generateRelationLink(array_flip($refs)); + $relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false); + $relations[$className][$relationName] = [ + "return \$this->hasOne($refClassName::className(), $link);", + $refClassName, + false, + ]; + + // Add relation for the referenced table + $hasMany = false; + foreach ($fks as $key) { + if (!in_array($key, $table->primaryKey, true)) { + $hasMany = true; + break; + } + } + $link = $this->generateRelationLink($refs); + $relationName = $this->generateRelationName($relations, $refClassName, $refTable, $className, $hasMany); + $relations[$refClassName][$relationName] = [ + "return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "($className::className(), $link);", + $className, + $hasMany, + ]; + } + + if (($fks = $this->checkPivotTable($table)) === false) { + continue; + } + $table0 = $fks[$table->primaryKey[0]][0]; + $table1 = $fks[$table->primaryKey[1]][0]; + $className0 = $this->generateClassName($table0); + $className1 = $this->generateClassName($table1); + + $link = $this->generateRelationLink([$fks[$table->primaryKey[1]][1] => $table->primaryKey[1]]); + $viaLink = $this->generateRelationLink([$table->primaryKey[0] => $fks[$table->primaryKey[0]][1]]); + $relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true); + $relations[$className0][$relationName] = [ + "return \$this->hasMany($className1::className(), $link)->viaTable('{$table->name}', $viaLink);", + $className1, + true, + ]; + + $link = $this->generateRelationLink([$fks[$table->primaryKey[0]][1] => $table->primaryKey[0]]); + $viaLink = $this->generateRelationLink([$table->primaryKey[1] => $fks[$table->primaryKey[1]][1]]); + $relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true); + $relations[$className1][$relationName] = [ + "return \$this->hasMany($className0::className(), $link)->viaTable('{$table->name}', $viaLink);", + $className0, + true, + ]; + } + + return $relations; + } + + /** + * Generates the link parameter to be used in generating the relation declaration. + * @param array $refs reference constraint + * @return string the generated link parameter. + */ + protected function generateRelationLink($refs) + { + $pairs = []; + foreach ($refs as $a => $b) { + $pairs[] = "'$a' => '$b'"; + } + + return '[' . implode(', ', $pairs) . ']'; + } + + /** + * Checks if the given table is a pivot table. + * For simplicity, this method only deals with the case where the pivot contains two PK columns, + * each referencing a column in a different table. + * @param \yii\db\TableSchema the table being checked + * @return array|boolean the relevant foreign key constraint information if the table is a pivot table, + * or false if the table is not a pivot table. + */ + protected function checkPivotTable($table) + { + $pk = $table->primaryKey; + if (count($pk) !== 2) { + return false; + } + $fks = []; + foreach ($table->foreignKeys as $refs) { + if (count($refs) === 2) { + if (isset($refs[$pk[0]])) { + $fks[$pk[0]] = [$refs[0], $refs[$pk[0]]]; + } elseif (isset($refs[$pk[1]])) { + $fks[$pk[1]] = [$refs[0], $refs[$pk[1]]]; + } + } + } + if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) { + return $fks; + } else { + return false; + } + } + + /** + * Generate a relation name for the specified table and a base name. + * @param array $relations the relations being generated currently. + * @param string $className the class name that will contain the relation declarations + * @param \yii\db\TableSchema $table the table schema + * @param string $key a base name that the relation name may be generated from + * @param boolean $multiple whether this is a has-many relation + * @return string the relation name + */ + protected function generateRelationName($relations, $className, $table, $key, $multiple) + { + if (strcasecmp(substr($key, -2), 'id') === 0 && strcasecmp($key, 'id')) { + $key = rtrim(substr($key, 0, -2), '_'); + } + if ($multiple) { + $key = Inflector::pluralize($key); + } + $name = $rawName = Inflector::id2camel($key, '_'); + $i = 0; + while (isset($table->columns[lcfirst($name)])) { + $name = $rawName . ($i++); + } + while (isset($relations[$className][lcfirst($name)])) { + $name = $rawName . ($i++); + } + + return $name; + } + + /** + * Validates the [[db]] attribute. + */ + public function validateDb() + { + if (Yii::$app->hasComponent($this->db) === false) { + $this->addError('db', 'There is no application component named "db".'); + } elseif (!Yii::$app->getComponent($this->db) instanceof Connection) { + $this->addError('db', 'The "db" application component must be a DB connection instance.'); + } + } + + /** + * Validates the [[ns]] attribute. + */ + public function validateNamespace() + { + $this->ns = ltrim($this->ns, '\\'); + $path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false); + if ($path === false) { + $this->addError('ns', 'Namespace must be associated with an existing directory.'); + } + } + + /** + * Validates the [[modelClass]] attribute. + */ + public function validateModelClass() + { + if ($this->isReservedKeyword($this->modelClass)) { + $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.'); + } + if (substr($this->tableName, -1) !== '*' && $this->modelClass == '') { + $this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.'); + } + } + + /** + * Validates the [[tableName]] attribute. + */ + public function validateTableName() + { + if (strpos($this->tableName, '*') !== false && substr($this->tableName, -1) !== '*') { + $this->addError('tableName', 'Asterisk is only allowed as the last character.'); + + return; + } + $tables = $this->getTableNames(); + if (empty($tables)) { + $this->addError('tableName', "Table '{$this->tableName}' does not exist."); + } else { + foreach ($tables as $table) { + $class = $this->generateClassName($table); + if ($this->isReservedKeyword($class)) { + $this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword."); + break; + } + } + } + } + + private $_tableNames; + private $_classNames; + + /** + * @return array the table names that match the pattern specified by [[tableName]]. + */ + protected function getTableNames() + { + if ($this->_tableNames !== null) { + return $this->_tableNames; + } + $db = $this->getDbConnection(); + if ($db === null) { + return []; + } + $tableNames = []; + if (strpos($this->tableName, '*') !== false) { + if (($pos = strrpos($this->tableName, '.')) !== false) { + $schema = substr($this->tableName, 0, $pos); + $pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/'; + } else { + $schema = ''; + $pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/'; + } + + foreach ($db->schema->getTableNames($schema) as $table) { + if (preg_match($pattern, $table)) { + $tableNames[] = $schema === '' ? $table : ($schema . '.' . $table); + } + } + } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) { + $tableNames[] = $this->tableName; + $this->_classNames[$this->tableName] = $this->modelClass; + } + + return $this->_tableNames = $tableNames; + } + + /** + * Generates a class name from the specified table name. + * @param string $tableName the table name (which may contain schema prefix) + * @return string the generated class name + */ + protected function generateClassName($tableName) + { + if (isset($this->_classNames[$tableName])) { + return $this->_classNames[$tableName]; + } + + if (($pos = strrpos($tableName, '.')) !== false) { + $tableName = substr($tableName, $pos + 1); + } + + $db = $this->getDbConnection(); + $patterns = []; + $patterns[] = "/^{$db->tablePrefix}(.*?)$/"; + $patterns[] = "/^(.*?){$db->tablePrefix}$/"; + if (strpos($this->tableName, '*') !== false) { + $pattern = $this->tableName; + if (($pos = strrpos($pattern, '.')) !== false) { + $pattern = substr($pattern, $pos + 1); + } + $patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/'; + } + $className = $tableName; + foreach ($patterns as $pattern) { + if (preg_match($pattern, $tableName, $matches)) { + $className = $matches[1]; + break; + } + } + + return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); + } + + /** + * @return Connection the DB connection as specified by [[db]]. + */ + protected function getDbConnection() + { + return Yii::$app->{$this->db}; + } + + /** + * Checks if any of the specified columns of an unique index is auto incrementable. + * @param \yii\db\TableSchema $table the table schema + * @param array $columns columns to check for autoIncrement property + * @return boolean whether any of the specified columns is auto incrementable. + */ + protected function isUniqueColumnAutoIncrementable($table, $columns) + { + foreach ($columns as $column) { + if ($table->columns[$column]->autoIncrement) { + return true; + } + } + + return false; + } } diff --git a/extensions/gii/generators/model/templates/model.php b/extensions/gii/generators/model/templates/model.php index b558f0f47fb..24909d0a4b2 100644 --- a/extensions/gii/generators/model/templates/model.php +++ b/extensions/gii/generators/model/templates/model.php @@ -32,41 +32,41 @@ */ class extends baseClass, '\\') . "\n" ?> { - /** - * @inheritdoc - */ - public static function tableName() - { - return ''; - } + /** + * @inheritdoc + */ + public static function tableName() + { + return ''; + } - /** - * @inheritdoc - */ - public function rules() - { - return []; - } + /** + * @inheritdoc + */ + public function rules() + { + return []; + } - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ $label): ?> - '" . addslashes($label) . "',\n" ?> + '" . addslashes($label) . "',\n" ?> - ]; - } + ]; + } $relation): ?> - /** - * @return \yii\db\ActiveQuery - */ - public function get() - { - - } + /** + * @return \yii\db\ActiveQuery + */ + public function get() + { + + } } diff --git a/extensions/gii/generators/module/Generator.php b/extensions/gii/generators/module/Generator.php index f8bdd12a535..353afe67cd9 100644 --- a/extensions/gii/generators/module/Generator.php +++ b/extensions/gii/generators/module/Generator.php @@ -23,146 +23,147 @@ */ class Generator extends \yii\gii\Generator { - public $moduleClass; - public $moduleID; - - /** - * @inheritdoc - */ - public function getName() - { - return 'Module Generator'; - } - - /** - * @inheritdoc - */ - public function getDescription() - { - return 'This generator helps you to generate the skeleton code needed by a Yii module.'; - } - - /** - * @inheritdoc - */ - public function rules() - { - return array_merge(parent::rules(), [ - [['moduleID', 'moduleClass'], 'filter', 'filter' => 'trim'], - [['moduleID', 'moduleClass'], 'required'], - [['moduleID'], 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'], - [['moduleClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], - [['moduleClass'], 'validateModuleClass'], - ]); - } - - /** - * @inheritdoc - */ - public function attributeLabels() - { - return [ - 'moduleID' => 'Module ID', - 'moduleClass' => 'Module Class', - ]; - } - - /** - * @inheritdoc - */ - public function hints() - { - return [ - 'moduleID' => 'This refers to the ID of the module, e.g., admin.', - 'moduleClass' => 'This is the fully qualified class name of the module, e.g., app\modules\admin\Module.', - ]; - } - - /** - * @inheritdoc - */ - public function successMessage() - { - if (Yii::$app->hasModule($this->moduleID)) { - $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($this->moduleID), ['target' => '_blank']); - return "The module has been generated successfully. You may $link."; - } - - $output = << 'trim'], + [['moduleID', 'moduleClass'], 'required'], + [['moduleID'], 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'], + [['moduleClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], + [['moduleClass'], 'validateModuleClass'], + ]); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'moduleID' => 'Module ID', + 'moduleClass' => 'Module Class', + ]; + } + + /** + * @inheritdoc + */ + public function hints() + { + return [ + 'moduleID' => 'This refers to the ID of the module, e.g., admin.', + 'moduleClass' => 'This is the fully qualified class name of the module, e.g., app\modules\admin\Module.', + ]; + } + + /** + * @inheritdoc + */ + public function successMessage() + { + if (Yii::$app->hasModule($this->moduleID)) { + $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($this->moduleID), ['target' => '_blank']); + + return "The module has been generated successfully. You may $link."; + } + + $output = <<The module has been generated successfully.

    To access the module, you need to add this to your application configuration:

    EOD; - $code = << [ - '{$this->moduleID}' => [ - 'class' => '{$this->moduleClass}', - ], - ], - ...... + ...... + 'modules' => [ + '{$this->moduleID}' => [ + 'class' => '{$this->moduleClass}', + ], + ], + ...... EOD; - return $output . '
    ' . highlight_string($code, true) . '
    '; - } - - /** - * @inheritdoc - */ - public function requiredTemplates() - { - return ['module.php', 'controller.php', 'view.php']; - } - - /** - * @inheritdoc - */ - public function generate() - { - $files = []; - $modulePath = $this->getModulePath(); - $files[] = new CodeFile( - $modulePath . '/' . StringHelper::basename($this->moduleClass) . '.php', - $this->render("module.php") - ); - $files[] = new CodeFile( - $modulePath . '/controllers/DefaultController.php', - $this->render("controller.php") - ); - $files[] = new CodeFile( - $modulePath . '/views/default/index.php', - $this->render("view.php") - ); - - return $files; - } - - /** - * Validates [[moduleClass]] to make sure it is a fully qualified class name. - */ - public function validateModuleClass() - { - if (strpos($this->moduleClass, '\\') === false || Yii::getAlias('@' . str_replace('\\', '/', $this->moduleClass), false) === false) { - $this->addError('moduleClass', 'Module class must be properly namespaced.'); - } - if (substr($this->moduleClass, -1, 1) == '\\') { - $this->addError('moduleClass', 'Module class name must not be empty. Please enter a fully qualified class name. e.g. "app\\modules\\admin\\Module".'); - } - } - - /** - * @return boolean the directory that contains the module class - */ - public function getModulePath() - { - return Yii::getAlias('@' . str_replace('\\', '/', substr($this->moduleClass, 0, strrpos($this->moduleClass, '\\')))); - } - - /** - * @return string the controller namespace of the module. - */ - public function getControllerNamespace() - { - return substr($this->moduleClass, 0, strrpos($this->moduleClass, '\\')) . '\controllers'; - } + return $output . '
    ' . highlight_string($code, true) . '
    '; + } + + /** + * @inheritdoc + */ + public function requiredTemplates() + { + return ['module.php', 'controller.php', 'view.php']; + } + + /** + * @inheritdoc + */ + public function generate() + { + $files = []; + $modulePath = $this->getModulePath(); + $files[] = new CodeFile( + $modulePath . '/' . StringHelper::basename($this->moduleClass) . '.php', + $this->render("module.php") + ); + $files[] = new CodeFile( + $modulePath . '/controllers/DefaultController.php', + $this->render("controller.php") + ); + $files[] = new CodeFile( + $modulePath . '/views/default/index.php', + $this->render("view.php") + ); + + return $files; + } + + /** + * Validates [[moduleClass]] to make sure it is a fully qualified class name. + */ + public function validateModuleClass() + { + if (strpos($this->moduleClass, '\\') === false || Yii::getAlias('@' . str_replace('\\', '/', $this->moduleClass), false) === false) { + $this->addError('moduleClass', 'Module class must be properly namespaced.'); + } + if (substr($this->moduleClass, -1, 1) == '\\') { + $this->addError('moduleClass', 'Module class name must not be empty. Please enter a fully qualified class name. e.g. "app\\modules\\admin\\Module".'); + } + } + + /** + * @return boolean the directory that contains the module class + */ + public function getModulePath() + { + return Yii::getAlias('@' . str_replace('\\', '/', substr($this->moduleClass, 0, strrpos($this->moduleClass, '\\')))); + } + + /** + * @return string the controller namespace of the module. + */ + public function getControllerNamespace() + { + return substr($this->moduleClass, 0, strrpos($this->moduleClass, '\\')) . '\controllers'; + } } diff --git a/extensions/gii/generators/module/form.php b/extensions/gii/generators/module/form.php index 2874d9092d3..eec21d45742 100644 --- a/extensions/gii/generators/module/form.php +++ b/extensions/gii/generators/module/form.php @@ -7,7 +7,7 @@ ?>
    field($generator, 'moduleClass'); - echo $form->field($generator, 'moduleID'); + echo $form->field($generator, 'moduleClass'); + echo $form->field($generator, 'moduleID'); ?>
    diff --git a/extensions/gii/generators/module/templates/controller.php b/extensions/gii/generators/module/templates/controller.php index 018450c208d..ff7a095b20a 100644 --- a/extensions/gii/generators/module/templates/controller.php +++ b/extensions/gii/generators/module/templates/controller.php @@ -14,8 +14,8 @@ class DefaultController extends Controller { - public function actionIndex() - { - return $this->render('index'); - } + public function actionIndex() + { + return $this->render('index'); + } } diff --git a/extensions/gii/generators/module/templates/module.php b/extensions/gii/generators/module/templates/module.php index 72b32be8c5b..dabbc745b58 100644 --- a/extensions/gii/generators/module/templates/module.php +++ b/extensions/gii/generators/module/templates/module.php @@ -15,15 +15,14 @@ namespace ; - class extends \yii\base\Module { - public $controllerNamespace = 'getControllerNamespace() ?>'; + public $controllerNamespace = 'getControllerNamespace() ?>'; - public function init() - { - parent::init(); + public function init() + { + parent::init(); - // custom initialization code goes here - } + // custom initialization code goes here + } } diff --git a/extensions/gii/generators/module/templates/view.php b/extensions/gii/generators/module/templates/view.php index f5f42487c6b..7655ae49c02 100644 --- a/extensions/gii/generators/module/templates/view.php +++ b/extensions/gii/generators/module/templates/view.php @@ -5,14 +5,14 @@ */ ?>
    -

    $this->context->action->uniqueId ?>

    -

    - This is the view content for action "$this->context->action->id ?>". - The action belongs to the controller "get_class($this->context) ?>" - in the "$this->context->module->id ?>" module. -

    -

    - You may customize this page by editing the following file:
    - __FILE__ ?> -

    +

    $this->context->action->uniqueId ?>

    +

    + This is the view content for action "$this->context->action->id ?>". + The action belongs to the controller "get_class($this->context) ?>" + in the "$this->context->module->id ?>" module. +

    +

    + You may customize this page by editing the following file:
    + __FILE__ ?> +

    diff --git a/extensions/gii/views/default/diff.php b/extensions/gii/views/default/diff.php index 6ee91a108e3..5f88fa6c7c9 100644 --- a/extensions/gii/views/default/diff.php +++ b/extensions/gii/views/default/diff.php @@ -5,11 +5,11 @@ */ ?>
    - -
    Diff is not supported for this file type.
    - -
    Identical.
    - -
    - + +
    Diff is not supported for this file type.
    + +
    Identical.
    + +
    +
    diff --git a/extensions/gii/views/default/index.php b/extensions/gii/views/default/index.php index 67856c4917a..0ba65299f2c 100644 --- a/extensions/gii/views/default/index.php +++ b/extensions/gii/views/default/index.php @@ -10,22 +10,22 @@ $this->title = 'Welcome to Gii'; ?>
    - + -

    Start the fun with the following code generators:

    +

    Start the fun with the following code generators:

    -
    - $generator): ?> -
    -

    getName()) ?>

    -

    getDescription() ?>

    -

    $id], ['class' => 'btn btn-default']) ?>

    -
    - -
    +
    + $generator): ?> +
    +

    getName()) ?>

    +

    getDescription() ?>

    +

    $id], ['class' => 'btn btn-default']) ?>

    +
    + +
    -

    Get More Generators

    +

    Get More Generators

    diff --git a/extensions/gii/views/default/view.php b/extensions/gii/views/default/view.php index 2e1c7ffd7b4..a2ac3ac4e5d 100644 --- a/extensions/gii/views/default/view.php +++ b/extensions/gii/views/default/view.php @@ -19,55 +19,55 @@ $this->title = $generator->getName(); $templates = []; foreach ($generator->templates as $name => $path) { - $templates[$name] = "$name ($path)"; + $templates[$name] = "$name ($path)"; } ?>
    -

    title) ?>

    +

    title) ?>

    -

    getDescription() ?>

    +

    getDescription() ?>

    - "$id-generator", - 'successCssClass' => '', - 'fieldConfig' => ['class' => ActiveField::className()], - ]); ?> -
    -
    - renderFile($generator->formView(), [ - 'generator' => $generator, - 'form' => $form, - ]) ?> - field($generator, 'template')->sticky() - ->label('Code Template') - ->dropDownList($templates)->hint(' - Please select which set of the templates should be used to generated the code. - ') ?> -
    - 'preview', 'class' => 'btn btn-primary']) ?> + "$id-generator", + 'successCssClass' => '', + 'fieldConfig' => ['class' => ActiveField::className()], + ]); ?> +
    +
    + renderFile($generator->formView(), [ + 'generator' => $generator, + 'form' => $form, + ]) ?> + field($generator, 'template')->sticky() + ->label('Code Template') + ->dropDownList($templates)->hint(' + Please select which set of the templates should be used to generated the code. + ') ?> +
    + 'preview', 'class' => 'btn btn-primary']) ?> - - 'generate', 'class' => 'btn btn-success']) ?> - -
    -
    -
    + + 'generate', 'class' => 'btn btn-success']) ?> + +
    +
    +
    - render('view/results', [ - 'generator' => $generator, - 'results' => $results, - 'hasError' => $hasError, - ]); - } elseif (isset($files)) { - echo $this->render('view/files', [ - 'id' => $id, - 'generator' => $generator, - 'files' => $files, - 'answers' => $answers, - ]); - } - ?> - + render('view/results', [ + 'generator' => $generator, + 'results' => $results, + 'hasError' => $hasError, + ]); + } elseif (isset($files)) { + echo $this->render('view/files', [ + 'id' => $id, + 'generator' => $generator, + 'files' => $files, + 'answers' => $answers, + ]); + } + ?> +
    diff --git a/extensions/gii/views/default/view/files.php b/extensions/gii/views/default/view/files.php index 0e83e5481e4..2f0499e7689 100644 --- a/extensions/gii/views/default/view/files.php +++ b/extensions/gii/views/default/view/files.php @@ -12,89 +12,89 @@ */ ?>
    -

    Click on the above Generate button to generate the files selected below:

    +

    Click on the above Generate button to generate the files selected below:

    - - - - - - operation !== CodeFile::OP_SKIP) { - $fileChangeExists = true; - echo ''; - break; - } - } - ?> - - - - - - operation === CodeFile::OP_OVERWRITE) { - $trClass = 'warning'; - } elseif ($file->operation === CodeFile::OP_SKIP) { - $trClass = 'active'; - } elseif ($file->operation === CodeFile::OP_CREATE) { - $trClass = 'success'; - } else { - $trClass = ''; - } - ?> - operation $trClass" ?>"> - - - - - - - - -
    Code FileAction
    - getRelativePath()), ['preview', 'id' => $id, 'file' => $file->id], ['class' => 'preview-code', 'data-title' => $file->getRelativePath()]) ?> - operation === CodeFile::OP_OVERWRITE): ?> - $id, 'file' => $file->id], ['class' => 'diff-code label label-warning', 'data-title' => $file->getRelativePath()]) ?> - - - operation === CodeFile::OP_SKIP) { - echo 'unchanged'; - } else { - echo $file->operation; - } - ?> - - operation === CodeFile::OP_SKIP) { - echo ' '; - } else { - echo Html::checkBox("answers[{$file->id}]", isset($answers) ? isset($answers[$file->id]) : ($file->operation === CodeFile::OP_CREATE)); - } - ?> -
    + + + + + + operation !== CodeFile::OP_SKIP) { + $fileChangeExists = true; + echo ''; + break; + } + } + ?> - + + + + + operation === CodeFile::OP_OVERWRITE) { + $trClass = 'warning'; + } elseif ($file->operation === CodeFile::OP_SKIP) { + $trClass = 'active'; + } elseif ($file->operation === CodeFile::OP_CREATE) { + $trClass = 'success'; + } else { + $trClass = ''; + } + ?> + operation $trClass" ?>"> + + + + + + + + +
    Code FileAction
    + getRelativePath()), ['preview', 'id' => $id, 'file' => $file->id], ['class' => 'preview-code', 'data-title' => $file->getRelativePath()]) ?> + operation === CodeFile::OP_OVERWRITE): ?> + $id, 'file' => $file->id], ['class' => 'diff-code label label-warning', 'data-title' => $file->getRelativePath()]) ?> + + + operation === CodeFile::OP_SKIP) { + echo 'unchanged'; + } else { + echo $file->operation; + } + ?> + + operation === CodeFile::OP_SKIP) { + echo ' '; + } else { + echo Html::checkBox("answers[{$file->id}]", isset($answers) ? isset($answers[$file->id]) : ($file->operation === CodeFile::OP_CREATE)); + } + ?> +
    + +
    diff --git a/extensions/gii/views/default/view/results.php b/extensions/gii/views/default/view/results.php index 0e9b7d9f985..b324b361f60 100644 --- a/extensions/gii/views/default/view/results.php +++ b/extensions/gii/views/default/view/results.php @@ -7,12 +7,12 @@ */ ?>
    - There was something wrong when generating the code. Please check the following messages.
    '; - } else { - echo '
    ' . $generator->successMessage() . '
    '; - } - ?> -
    + There was something wrong when generating the code. Please check the following messages.
    '; + } else { + echo '
    ' . $generator->successMessage() . '
    '; + } + ?> +
    diff --git a/extensions/gii/views/layouts/generator.php b/extensions/gii/views/layouts/generator.php index d4c205aa442..dfb40872ba9 100644 --- a/extensions/gii/views/layouts/generator.php +++ b/extensions/gii/views/layouts/generator.php @@ -12,20 +12,20 @@ ?> beginContent('@yii/gii/views/layouts/main.php'); ?>
    -
    -
    - $generator) { - $label = '' . Html::encode($generator->getName()); - echo Html::a($label, ['default/view', 'id' => $id], [ - 'class' => $generator === $activeGenerator ? 'list-group-item active' : 'list-group-item', - ]); - } - ?> -
    -
    -
    - -
    +
    +
    + $generator) { + $label = '' . Html::encode($generator->getName()); + echo Html::a($label, ['default/view', 'id' => $id], [ + 'class' => $generator === $activeGenerator ? 'list-group-item active' : 'list-group-item', + ]); + } + ?> +
    +
    +
    + +
    endContent(); ?> diff --git a/extensions/gii/views/layouts/main.php b/extensions/gii/views/layouts/main.php index f324e417e58..7f9aca770ff 100644 --- a/extensions/gii/views/layouts/main.php +++ b/extensions/gii/views/layouts/main.php @@ -13,39 +13,39 @@ - - - <?= Html::encode($this->title) ?> - head() ?> + + + <?= Html::encode($this->title) ?> + head() ?> beginBody() ?> Html::img($asset->baseUrl . '/logo.png'), - 'brandUrl' => ['default/index'], - 'options' => ['class' => 'navbar-inverse navbar-fixed-top'], + 'brandLabel' => Html::img($asset->baseUrl . '/logo.png'), + 'brandUrl' => ['default/index'], + 'options' => ['class' => 'navbar-inverse navbar-fixed-top'], ]); echo Nav::widget([ - 'options' => ['class' => 'nav navbar-nav navbar-right'], - 'items' => [ - ['label' => 'Home', 'url' => ['default/index']], - ['label' => 'Help', 'url' => 'https://github.com/yiisoft/yii2/blob/master/docs/guide/gii.md'], - ['label' => 'Application', 'url' => Yii::$app->homeUrl], - ], + 'options' => ['class' => 'nav navbar-nav navbar-right'], + 'items' => [ + ['label' => 'Home', 'url' => ['default/index']], + ['label' => 'Help', 'url' => 'https://github.com/yiisoft/yii2/blob/master/docs/guide/gii.md'], + ['label' => 'Application', 'url' => Yii::$app->homeUrl], + ], ]); NavBar::end(); ?>
    - +
    endBody() ?> diff --git a/extensions/imagine/BaseImage.php b/extensions/imagine/BaseImage.php index bf6ed773669..527e7e6be5a 100644 --- a/extensions/imagine/BaseImage.php +++ b/extensions/imagine/BaseImage.php @@ -29,227 +29,229 @@ */ class BaseImage { - /** - * GD2 driver definition for Imagine implementation using the GD library. - */ - const DRIVER_GD2 = 'gd2'; - /** - * imagick driver definition. - */ - const DRIVER_IMAGICK = 'imagick'; - /** - * gmagick driver definition. - */ - const DRIVER_GMAGICK = 'gmagick'; - /** - * @var array|string the driver to use. This can be either a single driver name or an array of driver names. - * If the latter, the first available driver will be used. - */ - public static $driver = [self::DRIVER_GMAGICK, self::DRIVER_IMAGICK, self::DRIVER_GD2]; - - /** - * @var ImagineInterface instance. - */ - private static $_imagine; - - /** - * Returns the `Imagine` object that supports various image manipulations. - * @return ImagineInterface the `Imagine` object - */ - public static function getImagine() - { - if (self::$_imagine === null) { - self::$_imagine = static::createImagine(); - } - return self::$_imagine; - } - - /** - * @param ImagineInterface $imagine the `Imagine` object. - */ - public static function setImagine($imagine) - { - self::$_imagine = $imagine; - } - - /** - * Creates an `Imagine` object based on the specified [[driver]]. - * @return ImagineInterface the new `Imagine` object - * @throws InvalidConfigException if [[driver]] is unknown or the system doesn't support any [[driver]]. - */ - protected static function createImagine() - { - foreach ((array)static::$driver as $driver) { - switch ($driver) { - case self::DRIVER_GMAGICK: - if (class_exists('Gmagick', false)) { - return new \Imagine\Gmagick\Imagine(); - } - break; - case self::DRIVER_IMAGICK: - if (class_exists('Imagick', false)) { - return new \Imagine\Imagick\Imagine(); - } - break; - case self::DRIVER_GD2: - if (function_exists('gd_info')) { - return new \Imagine\Gd\Imagine(); - } - break; - default: - throw new InvalidConfigException("Unknown driver: $driver"); - } - } - throw new InvalidConfigException("Your system does not support any of these drivers: " . implode(',', (array)static::$driver)); - } - - /** - * Crops an image. - * - * For example, - * - * ~~~ - * $obj->crop('path\to\image.jpg', 200, 200, [5, 5]); - * - * $point = new \Imagine\Image\Point(5, 5); - * $obj->crop('path\to\image.jpg', 200, 200, $point); - * ~~~ - * - * @param string $filename the image file path or path alias. - * @param integer $width the crop width - * @param integer $height the crop height - * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates. - * @return ImageInterface - * @throws InvalidParamException if the `$start` parameter is invalid - */ - public static function crop($filename, $width, $height, array $start = [0, 0]) - { - if (!isset($start[0], $start[1])) { - throw new InvalidParamException('$start must be an array of two elements.'); - } - - return static::getImagine() - ->open(Yii::getAlias($filename)) - ->copy() - ->crop(new Point($start[0], $start[1]), new Box($width, $height)); - } - - /** - * Creates a thumbnail image. The function differs from [[\Imagine\Image\ImageInterface::thumbnail()]] function that - * it keeps the aspect ratio of the image. - * @param string $filename the image file path or path alias. - * @param integer $width the width in pixels to create the thumbnail - * @param integer $height the height in pixels to create the thumbnail - * @param string $mode - * @return ImageInterface - */ - public static function thumbnail($filename, $width, $height, $mode = ManipulatorInterface::THUMBNAIL_OUTBOUND) - { - $box = new Box($width, $height); - $img = static::getImagine()->open(Yii::getAlias($filename)); - - if (($img->getSize()->getWidth() <= $box->getWidth() && $img->getSize()->getHeight() <= $box->getHeight()) || (!$box->getWidth() && !$box->getHeight())) { - return $img->copy(); - } - - $img = $img->thumbnail($box, $mode); - - // create empty image to preserve aspect ratio of thumbnail - $thumb = static::getImagine()->create($box, new Color('FFF', 100)); - - // calculate points - $size = $img->getSize(); - - $startX = 0; - $startY = 0; - if ($size->getWidth() < $width) { - $startX = ceil($width - $size->getWidth()) / 2; - } - if ($size->getHeight() < $height) { - $startY = ceil($height - $size->getHeight()) / 2; - } - - $thumb->paste($img, new Point($startX, $startY)); - - return $thumb; - } - - /** - * Adds a watermark to an existing image. - * @param string $filename the image file path or path alias. - * @param string $watermarkFilename the file path or path alias of the watermark image. - * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates. - * @return ImageInterface - * @throws InvalidParamException if `$start` is invalid - */ - public static function watermark($filename, $watermarkFilename, array $start = [0, 0]) - { - if (!isset($start[0], $start[1])) { - throw new InvalidParamException('$start must be an array of two elements.'); - } - - $img = static::getImagine()->open(Yii::getAlias($filename)); - $watermark = static::getImagine()->open(Yii::getAlias($watermarkFilename)); - $img->paste($watermark, new Point($start[0], $start[1])); - return $img; - } - - /** - * Draws a text string on an existing image. - * @param string $filename the image file path or path alias. - * @param string $text the text to write to the image - * @param string $fontFile the file path or path alias - * @param array $start the starting position of the text. This must be an array with two elements representing `x` and `y` coordinates. - * @param array $fontOptions the font options. The following options may be specified: - * - * - color: The font color. Defaults to "fff". - * - size: The font size. Defaults to 12. - * - angle: The angle to use to write the text. Defaults to 0. - * - * @return ImageInterface - * @throws InvalidParamException if `$fontOptions` is invalid - */ - public static function text($filename, $text, $fontFile, array $start = [0, 0], array $fontOptions = []) - { - if (!isset($start[0], $start[1])) { - throw new InvalidParamException('$start must be an array of two elements.'); - } - - $fontSize = ArrayHelper::getValue($fontOptions, 'size', 12); - $fontColor = ArrayHelper::getValue($fontOptions, 'color', 'fff'); - $fontAngle = ArrayHelper::getValue($fontOptions, 'angle', 0); - - $img = static::getImagine()->open(Yii::getAlias($filename)); - $font = static::getImagine()->font(Yii::getAlias($fontFile), $fontSize, new Color($fontColor)); - - $img->draw()->text($text, $font, new Point($start[0], $start[1]), $fontAngle); - - return $img; - } - - /** - * Adds a frame around of the image. Please note that the image size will increase by `$margin` x 2. - * @param string $filename the full path to the image file - * @param integer $margin the frame size to add around the image - * @param string $color the frame color - * @param integer $alpha the alpha value of the frame. - * @return ImageInterface - */ - public static function frame($filename, $margin = 20, $color = '666', $alpha = 100) - { - $img = static::getImagine()->open(Yii::getAlias($filename)); - - $size = $img->getSize(); - - $pasteTo = new Point($margin, $margin); - $padColor = new Color($color, $alpha); - - $box = new Box($size->getWidth() + ceil($margin * 2), $size->getHeight() + ceil($margin * 2)); - - $image = static::getImagine()->create($box, $padColor); - - $image->paste($img, $pasteTo); - - return $image; - } + /** + * GD2 driver definition for Imagine implementation using the GD library. + */ + const DRIVER_GD2 = 'gd2'; + /** + * imagick driver definition. + */ + const DRIVER_IMAGICK = 'imagick'; + /** + * gmagick driver definition. + */ + const DRIVER_GMAGICK = 'gmagick'; + /** + * @var array|string the driver to use. This can be either a single driver name or an array of driver names. + * If the latter, the first available driver will be used. + */ + public static $driver = [self::DRIVER_GMAGICK, self::DRIVER_IMAGICK, self::DRIVER_GD2]; + + /** + * @var ImagineInterface instance. + */ + private static $_imagine; + + /** + * Returns the `Imagine` object that supports various image manipulations. + * @return ImagineInterface the `Imagine` object + */ + public static function getImagine() + { + if (self::$_imagine === null) { + self::$_imagine = static::createImagine(); + } + + return self::$_imagine; + } + + /** + * @param ImagineInterface $imagine the `Imagine` object. + */ + public static function setImagine($imagine) + { + self::$_imagine = $imagine; + } + + /** + * Creates an `Imagine` object based on the specified [[driver]]. + * @return ImagineInterface the new `Imagine` object + * @throws InvalidConfigException if [[driver]] is unknown or the system doesn't support any [[driver]]. + */ + protected static function createImagine() + { + foreach ((array) static::$driver as $driver) { + switch ($driver) { + case self::DRIVER_GMAGICK: + if (class_exists('Gmagick', false)) { + return new \Imagine\Gmagick\Imagine(); + } + break; + case self::DRIVER_IMAGICK: + if (class_exists('Imagick', false)) { + return new \Imagine\Imagick\Imagine(); + } + break; + case self::DRIVER_GD2: + if (function_exists('gd_info')) { + return new \Imagine\Gd\Imagine(); + } + break; + default: + throw new InvalidConfigException("Unknown driver: $driver"); + } + } + throw new InvalidConfigException("Your system does not support any of these drivers: " . implode(',', (array) static::$driver)); + } + + /** + * Crops an image. + * + * For example, + * + * ~~~ + * $obj->crop('path\to\image.jpg', 200, 200, [5, 5]); + * + * $point = new \Imagine\Image\Point(5, 5); + * $obj->crop('path\to\image.jpg', 200, 200, $point); + * ~~~ + * + * @param string $filename the image file path or path alias. + * @param integer $width the crop width + * @param integer $height the crop height + * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates. + * @return ImageInterface + * @throws InvalidParamException if the `$start` parameter is invalid + */ + public static function crop($filename, $width, $height, array $start = [0, 0]) + { + if (!isset($start[0], $start[1])) { + throw new InvalidParamException('$start must be an array of two elements.'); + } + + return static::getImagine() + ->open(Yii::getAlias($filename)) + ->copy() + ->crop(new Point($start[0], $start[1]), new Box($width, $height)); + } + + /** + * Creates a thumbnail image. The function differs from [[\Imagine\Image\ImageInterface::thumbnail()]] function that + * it keeps the aspect ratio of the image. + * @param string $filename the image file path or path alias. + * @param integer $width the width in pixels to create the thumbnail + * @param integer $height the height in pixels to create the thumbnail + * @param string $mode + * @return ImageInterface + */ + public static function thumbnail($filename, $width, $height, $mode = ManipulatorInterface::THUMBNAIL_OUTBOUND) + { + $box = new Box($width, $height); + $img = static::getImagine()->open(Yii::getAlias($filename)); + + if (($img->getSize()->getWidth() <= $box->getWidth() && $img->getSize()->getHeight() <= $box->getHeight()) || (!$box->getWidth() && !$box->getHeight())) { + return $img->copy(); + } + + $img = $img->thumbnail($box, $mode); + + // create empty image to preserve aspect ratio of thumbnail + $thumb = static::getImagine()->create($box, new Color('FFF', 100)); + + // calculate points + $size = $img->getSize(); + + $startX = 0; + $startY = 0; + if ($size->getWidth() < $width) { + $startX = ceil($width - $size->getWidth()) / 2; + } + if ($size->getHeight() < $height) { + $startY = ceil($height - $size->getHeight()) / 2; + } + + $thumb->paste($img, new Point($startX, $startY)); + + return $thumb; + } + + /** + * Adds a watermark to an existing image. + * @param string $filename the image file path or path alias. + * @param string $watermarkFilename the file path or path alias of the watermark image. + * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates. + * @return ImageInterface + * @throws InvalidParamException if `$start` is invalid + */ + public static function watermark($filename, $watermarkFilename, array $start = [0, 0]) + { + if (!isset($start[0], $start[1])) { + throw new InvalidParamException('$start must be an array of two elements.'); + } + + $img = static::getImagine()->open(Yii::getAlias($filename)); + $watermark = static::getImagine()->open(Yii::getAlias($watermarkFilename)); + $img->paste($watermark, new Point($start[0], $start[1])); + + return $img; + } + + /** + * Draws a text string on an existing image. + * @param string $filename the image file path or path alias. + * @param string $text the text to write to the image + * @param string $fontFile the file path or path alias + * @param array $start the starting position of the text. This must be an array with two elements representing `x` and `y` coordinates. + * @param array $fontOptions the font options. The following options may be specified: + * + * - color: The font color. Defaults to "fff". + * - size: The font size. Defaults to 12. + * - angle: The angle to use to write the text. Defaults to 0. + * + * @return ImageInterface + * @throws InvalidParamException if `$fontOptions` is invalid + */ + public static function text($filename, $text, $fontFile, array $start = [0, 0], array $fontOptions = []) + { + if (!isset($start[0], $start[1])) { + throw new InvalidParamException('$start must be an array of two elements.'); + } + + $fontSize = ArrayHelper::getValue($fontOptions, 'size', 12); + $fontColor = ArrayHelper::getValue($fontOptions, 'color', 'fff'); + $fontAngle = ArrayHelper::getValue($fontOptions, 'angle', 0); + + $img = static::getImagine()->open(Yii::getAlias($filename)); + $font = static::getImagine()->font(Yii::getAlias($fontFile), $fontSize, new Color($fontColor)); + + $img->draw()->text($text, $font, new Point($start[0], $start[1]), $fontAngle); + + return $img; + } + + /** + * Adds a frame around of the image. Please note that the image size will increase by `$margin` x 2. + * @param string $filename the full path to the image file + * @param integer $margin the frame size to add around the image + * @param string $color the frame color + * @param integer $alpha the alpha value of the frame. + * @return ImageInterface + */ + public static function frame($filename, $margin = 20, $color = '666', $alpha = 100) + { + $img = static::getImagine()->open(Yii::getAlias($filename)); + + $size = $img->getSize(); + + $pasteTo = new Point($margin, $margin); + $padColor = new Color($color, $alpha); + + $box = new Box($size->getWidth() + ceil($margin * 2), $size->getHeight() + ceil($margin * 2)); + + $image = static::getImagine()->create($box, $padColor); + + $image->paste($img, $pasteTo); + + return $image; + } } diff --git a/extensions/jui/Accordion.php b/extensions/jui/Accordion.php index a265cb87f8a..d4d4dcdbbaa 100644 --- a/extensions/jui/Accordion.php +++ b/extensions/jui/Accordion.php @@ -43,85 +43,84 @@ */ class Accordion extends Widget { - /** - * @var array the HTML attributes for the widget container tag. The following special options are recognized: - * - * - tag: string, defaults to "div", the tag name of the container tag of this widget - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array list of collapsible items. Each item can be an array of the following structure: - * - * ~~~ - * [ - * 'header' => 'Item header', - * 'content' => 'Item content', - * // the HTML attributes of the item header container tag. This will overwrite "headerOptions". - * 'headerOptions' => [], - * // the HTML attributes of the item container tag. This will overwrite "itemOptions". - * 'options' => [], - * ] - * ~~~ - */ - public $items = []; - /** - * @var array list of HTML attributes for the item container tags. This will be overwritten - * by the "options" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "div", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; - /** - * @var array list of HTML attributes for the item header container tags. This will be overwritten - * by the "headerOptions" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "h3", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $headerOptions = []; + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array list of collapsible items. Each item can be an array of the following structure: + * + * ~~~ + * [ + * 'header' => 'Item header', + * 'content' => 'Item content', + * // the HTML attributes of the item header container tag. This will overwrite "headerOptions". + * 'headerOptions' => [], + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => [], + * ] + * ~~~ + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * @var array list of HTML attributes for the item header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "h3", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('accordion', AccordionAsset::className()); + } - /** - * Renders the widget. - */ - public function run() - { - $options = $this->options; - $tag = ArrayHelper::remove($options, 'tag', 'div'); - echo Html::beginTag($tag, $options) . "\n"; - echo $this->renderItems() . "\n"; - echo Html::endTag($tag) . "\n"; - $this->registerWidget('accordion', AccordionAsset::className()); - } + /** + * Renders collapsible items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $items = []; + foreach ($this->items as $item) { + if (!isset($item['header'])) { + throw new InvalidConfigException("The 'header' option is required."); + } + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); + $headerTag = ArrayHelper::remove($headerOptions, 'tag', 'h3'); + $items[] = Html::tag($headerTag, $item['header'], $headerOptions); + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $items[] = Html::tag($tag, $item['content'], $options); + } - /** - * Renders collapsible items as specified on [[items]]. - * @return string the rendering result. - * @throws InvalidConfigException. - */ - protected function renderItems() - { - $items = []; - foreach ($this->items as $item) { - if (!isset($item['header'])) { - throw new InvalidConfigException("The 'header' option is required."); - } - if (!isset($item['content'])) { - throw new InvalidConfigException("The 'content' option is required."); - } - $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); - $headerTag = ArrayHelper::remove($headerOptions, 'tag', 'h3'); - $items[] = Html::tag($headerTag, $item['header'], $headerOptions); - $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); - $tag = ArrayHelper::remove($options, 'tag', 'div'); - $items[] = Html::tag($tag, $item['content'], $options); - } - - return implode("\n", $items); - } + return implode("\n", $items); + } } diff --git a/extensions/jui/AccordionAsset.php b/extensions/jui/AccordionAsset.php index 05c1e20d54d..3a71a703b4a 100644 --- a/extensions/jui/AccordionAsset.php +++ b/extensions/jui/AccordionAsset.php @@ -15,12 +15,12 @@ */ class AccordionAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.accordion.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\EffectAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.accordion.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; } diff --git a/extensions/jui/AutoComplete.php b/extensions/jui/AutoComplete.php index 93f4332f4ce..6617b2df869 100644 --- a/extensions/jui/AutoComplete.php +++ b/extensions/jui/AutoComplete.php @@ -41,25 +41,25 @@ */ class AutoComplete extends InputWidget { - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderWidget(); - $this->registerWidget('autocomplete', AutoCompleteAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('autocomplete', AutoCompleteAsset::className()); + } - /** - * Renders the AutoComplete widget. - * @return string the rendering result. - */ - public function renderWidget() - { - if ($this->hasModel()) { - return Html::activeTextInput($this->model, $this->attribute, $this->options); - } else { - return Html::textInput($this->name, $this->value, $this->options); - } - } + /** + * Renders the AutoComplete widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } } diff --git a/extensions/jui/AutoCompleteAsset.php b/extensions/jui/AutoCompleteAsset.php index 8e2b3f463aa..4a92c93d637 100644 --- a/extensions/jui/AutoCompleteAsset.php +++ b/extensions/jui/AutoCompleteAsset.php @@ -15,12 +15,12 @@ */ class AutoCompleteAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.autocomplete.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\MenuAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.autocomplete.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\MenuAsset', + ]; } diff --git a/extensions/jui/ButtonAsset.php b/extensions/jui/ButtonAsset.php index b00354e4ba3..fb662558c3a 100644 --- a/extensions/jui/ButtonAsset.php +++ b/extensions/jui/ButtonAsset.php @@ -15,11 +15,11 @@ */ class ButtonAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.button.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.button.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/CoreAsset.php b/extensions/jui/CoreAsset.php index f221f53eccd..7a95d592561 100644 --- a/extensions/jui/CoreAsset.php +++ b/extensions/jui/CoreAsset.php @@ -15,14 +15,14 @@ */ class CoreAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.core.js', - 'jquery.ui.widget.js', - 'jquery.ui.position.js', - 'jquery.ui.mouse.js', - ]; - public $depends = [ - 'yii\web\JqueryAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.core.js', + 'jquery.ui.widget.js', + 'jquery.ui.position.js', + 'jquery.ui.mouse.js', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + ]; } diff --git a/extensions/jui/DatePicker.php b/extensions/jui/DatePicker.php index 9e46624a1ed..83a26d8ef3d 100644 --- a/extensions/jui/DatePicker.php +++ b/extensions/jui/DatePicker.php @@ -45,83 +45,82 @@ */ class DatePicker extends InputWidget { - /** - * @var string the locale ID (eg 'fr', 'de') for the language to be used by the date picker. - * If this property is empty, then the current application language will be used. - */ - public $language; - /** - * @var boolean If true, shows the widget as an inline calendar and the input as a hidden field. - */ - public $inline = false; - /** - * @var array the HTML attributes for the container tag. This is only used when [[inline]] is true. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $containerOptions = []; + /** + * @var string the locale ID (eg 'fr', 'de') for the language to be used by the date picker. + * If this property is empty, then the current application language will be used. + */ + public $language; + /** + * @var boolean If true, shows the widget as an inline calendar and the input as a hidden field. + */ + public $inline = false; + /** + * @var array the HTML attributes for the container tag. This is only used when [[inline]] is true. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $containerOptions = []; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->inline && !isset($this->containerOptions['id'])) { + $this->containerOptions['id'] = $this->options['id'] . '-container'; + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->inline && !isset($this->containerOptions['id'])) { - $this->containerOptions['id'] = $this->options['id'] . '-container'; - } - } + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget() . "\n"; + $containerID = $this->inline ? $this->containerOptions['id'] : $this->options['id']; + $language = $this->language ? $this->language : Yii::$app->language; + if ($language != 'en') { + $view = $this->getView(); + DatePickerRegionalAsset::register($view); - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderWidget() . "\n"; - $containerID = $this->inline ? $this->containerOptions['id'] : $this->options['id']; - $language = $this->language ? $this->language : Yii::$app->language; - if ($language != 'en') { - $view = $this->getView(); - DatePickerRegionalAsset::register($view); + $options = Json::encode($this->clientOptions); + $view->registerJs("$('#{$containerID}').datepicker($.extend({}, $.datepicker.regional['{$language}'], $options));"); - $options = Json::encode($this->clientOptions); - $view->registerJs("$('#{$containerID}').datepicker($.extend({}, $.datepicker.regional['{$language}'], $options));"); + $options = $this->clientOptions; + $this->clientOptions = false; // the datepicker js widget is already registered + $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID); + $this->clientOptions = $options; + } else { + $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID); + } + } - $options = $this->clientOptions; - $this->clientOptions = false; // the datepicker js widget is already registered - $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID); - $this->clientOptions = $options; - } else { - $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID); - } - } + /** + * Renders the DatePicker widget. + * @return string the rendering result. + */ + protected function renderWidget() + { + $contents = []; - /** - * Renders the DatePicker widget. - * @return string the rendering result. - */ - protected function renderWidget() - { - $contents = []; + if ($this->inline === false) { + if ($this->hasModel()) { + $contents[] = Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + $contents[] = Html::textInput($this->name, $this->value, $this->options); + } + } else { + if ($this->hasModel()) { + $contents[] = Html::activeHiddenInput($this->model, $this->attribute, $this->options); + $this->clientOptions['defaultDate'] = $this->model->{$this->attribute}; + } else { + $contents[] = Html::hiddenInput($this->name, $this->value, $this->options); + $this->clientOptions['defaultDate'] = $this->value; + } + $this->clientOptions['altField'] = '#' . $this->options['id']; + $contents[] = Html::tag('div', null, $this->containerOptions); + } - if ($this->inline === false) { - if ($this->hasModel()) { - $contents[] = Html::activeTextInput($this->model, $this->attribute, $this->options); - } else { - $contents[] = Html::textInput($this->name, $this->value, $this->options); - } - } else { - if ($this->hasModel()) { - $contents[] = Html::activeHiddenInput($this->model, $this->attribute, $this->options); - $this->clientOptions['defaultDate'] = $this->model->{$this->attribute}; - } else { - $contents[] = Html::hiddenInput($this->name, $this->value, $this->options); - $this->clientOptions['defaultDate'] = $this->value; - } - $this->clientOptions['altField'] = '#' . $this->options['id']; - $contents[] = Html::tag('div', null, $this->containerOptions); - } - - return implode("\n", $contents); - } + return implode("\n", $contents); + } } diff --git a/extensions/jui/DatePickerAsset.php b/extensions/jui/DatePickerAsset.php index 68fb486a434..48ff0e7e79c 100644 --- a/extensions/jui/DatePickerAsset.php +++ b/extensions/jui/DatePickerAsset.php @@ -15,12 +15,12 @@ */ class DatePickerAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.datepicker.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\EffectAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.datepicker.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; } diff --git a/extensions/jui/DatePickerRegionalAsset.php b/extensions/jui/DatePickerRegionalAsset.php index fcb931b37f5..31dfe6ec87b 100644 --- a/extensions/jui/DatePickerRegionalAsset.php +++ b/extensions/jui/DatePickerRegionalAsset.php @@ -15,11 +15,11 @@ */ class DatePickerRegionalAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.datepicker-i18n.js', - ]; - public $depends = [ - 'yii\jui\DatePickerAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.datepicker-i18n.js', + ]; + public $depends = [ + 'yii\jui\DatePickerAsset', + ]; } diff --git a/extensions/jui/Dialog.php b/extensions/jui/Dialog.php index a5cbaf2b691..f7e49a8f6aa 100644 --- a/extensions/jui/Dialog.php +++ b/extensions/jui/Dialog.php @@ -32,21 +32,21 @@ */ class Dialog extends Widget { - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - echo Html::beginTag('div', $this->options) . "\n"; - } + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::endTag('div') . "\n"; - $this->registerWidget('dialog', DialogAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('dialog', DialogAsset::className()); + } } diff --git a/extensions/jui/DialogAsset.php b/extensions/jui/DialogAsset.php index da6a32cfe6c..f14a096f7ed 100644 --- a/extensions/jui/DialogAsset.php +++ b/extensions/jui/DialogAsset.php @@ -15,14 +15,14 @@ */ class DialogAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.dialog.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\ButtonAsset', - 'yii\jui\DraggableAsset', - 'yii\jui\ResizableAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.dialog.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\ButtonAsset', + 'yii\jui\DraggableAsset', + 'yii\jui\ResizableAsset', + ]; } diff --git a/extensions/jui/Draggable.php b/extensions/jui/Draggable.php index 02e4973b88a..f54971aad85 100644 --- a/extensions/jui/Draggable.php +++ b/extensions/jui/Draggable.php @@ -30,21 +30,21 @@ */ class Draggable extends Widget { - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - echo Html::beginTag('div', $this->options) . "\n"; - } + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::endTag('div') . "\n"; - $this->registerWidget('draggable', DraggableAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('draggable', DraggableAsset::className()); + } } diff --git a/extensions/jui/DraggableAsset.php b/extensions/jui/DraggableAsset.php index 152ea6e14dd..742ab04f72b 100644 --- a/extensions/jui/DraggableAsset.php +++ b/extensions/jui/DraggableAsset.php @@ -15,11 +15,11 @@ */ class DraggableAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.draggable.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.draggable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/Droppable.php b/extensions/jui/Droppable.php index 530e736f837..d0c247d8467 100644 --- a/extensions/jui/Droppable.php +++ b/extensions/jui/Droppable.php @@ -30,21 +30,21 @@ */ class Droppable extends Widget { - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - echo Html::beginTag('div', $this->options) . "\n"; - } + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::endTag('div') . "\n"; - $this->registerWidget('droppable', DroppableAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('droppable', DroppableAsset::className()); + } } diff --git a/extensions/jui/DroppableAsset.php b/extensions/jui/DroppableAsset.php index ae7afa865dd..377f9dac003 100644 --- a/extensions/jui/DroppableAsset.php +++ b/extensions/jui/DroppableAsset.php @@ -15,11 +15,11 @@ */ class DroppableAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.droppable.js', - ]; - public $depends = [ - 'yii\jui\DraggableAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.droppable.js', + ]; + public $depends = [ + 'yii\jui\DraggableAsset', + ]; } diff --git a/extensions/jui/EffectAsset.php b/extensions/jui/EffectAsset.php index d28347c9098..b8cade08e19 100644 --- a/extensions/jui/EffectAsset.php +++ b/extensions/jui/EffectAsset.php @@ -15,11 +15,11 @@ */ class EffectAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.effect-all.js', - ]; - public $depends = [ - 'yii\web\JqueryAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.effect-all.js', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + ]; } diff --git a/extensions/jui/InputWidget.php b/extensions/jui/InputWidget.php index 77201bad5b9..b668ca47cc9 100644 --- a/extensions/jui/InputWidget.php +++ b/extensions/jui/InputWidget.php @@ -19,44 +19,43 @@ */ class InputWidget extends Widget { - /** - * @var Model the data model that this widget is associated with. - */ - public $model; - /** - * @var string the model attribute that this widget is associated with. - */ - public $attribute; - /** - * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. - */ - public $name; - /** - * @var string the input value. - */ - public $value; + /** + * @var Model the data model that this widget is associated with. + */ + public $model; + /** + * @var string the model attribute that this widget is associated with. + */ + public $attribute; + /** + * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. + */ + public $name; + /** + * @var string the input value. + */ + public $value; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + if (!$this->hasModel() && $this->name === null) { + throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified."); + } + if ($this->hasModel() && !isset($this->options['id'])) { + $this->options['id'] = Html::getInputId($this->model, $this->attribute); + } + parent::init(); + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - if (!$this->hasModel() && $this->name === null) { - throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified."); - } - if ($this->hasModel() && !isset($this->options['id'])) { - $this->options['id'] = Html::getInputId($this->model, $this->attribute); - } - parent::init(); - } - - /** - * @return boolean whether this widget is associated with a data model. - */ - protected function hasModel() - { - return $this->model instanceof Model && $this->attribute !== null; - } + /** + * @return boolean whether this widget is associated with a data model. + */ + protected function hasModel() + { + return $this->model instanceof Model && $this->attribute !== null; + } } diff --git a/extensions/jui/Menu.php b/extensions/jui/Menu.php index bce215705e1..352634941ae 100644 --- a/extensions/jui/Menu.php +++ b/extensions/jui/Menu.php @@ -18,60 +18,59 @@ */ class Menu extends \yii\widgets\Menu { - /** - * @var array the options for the underlying jQuery UI widget. - * Please refer to the corresponding jQuery UI widget Web page for possible options. - * For example, [this page](http://api.jqueryui.com/accordion/) shows - * how to use the "Accordion" widget and the supported options (e.g. "header"). - */ - public $clientOptions = []; - /** - * @var array the event handlers for the underlying jQuery UI widget. - * Please refer to the corresponding jQuery UI widget Web page for possible events. - * For example, [this page](http://api.jqueryui.com/accordion/) shows - * how to use the "Accordion" widget and the supported events (e.g. "create"). - */ - public $clientEvents = []; + /** + * @var array the options for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible options. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported options (e.g. "header"). + */ + public $clientOptions = []; + /** + * @var array the event handlers for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible events. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported events (e.g. "create"). + */ + public $clientEvents = []; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - } + /** + * Renders the widget. + */ + public function run() + { + parent::run(); - /** - * Renders the widget. - */ - public function run() - { - parent::run(); + $view = $this->getView(); + MenuAsset::register($view); + /** @var \yii\web\AssetBundle $themeAsset */ + $themeAsset = Widget::$theme; + $themeAsset::register($view); - $view = $this->getView(); - MenuAsset::register($view); - /** @var \yii\web\AssetBundle $themeAsset */ - $themeAsset = Widget::$theme; - $themeAsset::register($view); + $id = $this->options['id']; + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').menu($options);"; + $view->registerJs($js); + } - $id = $this->options['id']; - if ($this->clientOptions !== false) { - $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); - $js = "jQuery('#$id').menu($options);"; - $view->registerJs($js); - } - - if (!empty($this->clientEvents)) { - $js = []; - foreach ($this->clientEvents as $event => $handler) { - $js[] = "jQuery('#$id').on('menu$event', $handler);"; - } - $view->registerJs(implode("\n", $js)); - } - } + if (!empty($this->clientEvents)) { + $js = []; + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('menu$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } } diff --git a/extensions/jui/MenuAsset.php b/extensions/jui/MenuAsset.php index c4c66ec049f..fab40d7ed89 100644 --- a/extensions/jui/MenuAsset.php +++ b/extensions/jui/MenuAsset.php @@ -15,11 +15,11 @@ */ class MenuAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.menu.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.menu.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/ProgressBar.php b/extensions/jui/ProgressBar.php index 1c555f18f4c..b90f6563cae 100644 --- a/extensions/jui/ProgressBar.php +++ b/extensions/jui/ProgressBar.php @@ -40,21 +40,21 @@ */ class ProgressBar extends Widget { - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - echo Html::beginTag('div', $this->options) . "\n"; - } + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::endTag('div') . "\n"; - $this->registerWidget('progressbar', ProgressBarAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('progressbar', ProgressBarAsset::className()); + } } diff --git a/extensions/jui/ProgressBarAsset.php b/extensions/jui/ProgressBarAsset.php index b7a5df29424..c607dc12683 100644 --- a/extensions/jui/ProgressBarAsset.php +++ b/extensions/jui/ProgressBarAsset.php @@ -15,11 +15,11 @@ */ class ProgressBarAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.progressbar.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.progressbar.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/Resizable.php b/extensions/jui/Resizable.php index bcff9a8bbdf..22e0ed59576 100644 --- a/extensions/jui/Resizable.php +++ b/extensions/jui/Resizable.php @@ -32,21 +32,21 @@ */ class Resizable extends Widget { - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); - echo Html::beginTag('div', $this->options) . "\n"; - } + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } - /** - * Renders the widget. - */ - public function run() - { - echo Html::endTag('div') . "\n"; - $this->registerWidget('resizable', ResizableAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('resizable', ResizableAsset::className()); + } } diff --git a/extensions/jui/ResizableAsset.php b/extensions/jui/ResizableAsset.php index 9097a8f63dc..a4e1d83417a 100644 --- a/extensions/jui/ResizableAsset.php +++ b/extensions/jui/ResizableAsset.php @@ -15,11 +15,11 @@ */ class ResizableAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.resizable.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.resizable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/Selectable.php b/extensions/jui/Selectable.php index 3055c03cabf..6af3f5815cb 100644 --- a/extensions/jui/Selectable.php +++ b/extensions/jui/Selectable.php @@ -48,73 +48,73 @@ */ class Selectable extends Widget { - /** - * @var array the HTML attributes for the widget container tag. The following special options are recognized: - * - * - tag: string, defaults to "ul", the tag name of the container tag of this widget. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array list of selectable items. Each item can be a string representing the item content - * or an array of the following structure: - * - * ~~~ - * [ - * 'content' => 'item content', - * // the HTML attributes of the item container tag. This will overwrite "itemOptions". - * 'options' => [], - * ] - * ~~~ - */ - public $items = []; - /** - * @var array list of HTML attributes for the item container tags. This will be overwritten - * by the "options" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "li", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the container tag of this widget. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array list of selectable items. Each item can be a string representing the item content + * or an array of the following structure: + * + * ~~~ + * [ + * 'content' => 'item content', + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => [], + * ] + * ~~~ + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('selectable', SelectableAsset::className()); + } - /** - * Renders the widget. - */ - public function run() - { - $options = $this->options; - $tag = ArrayHelper::remove($options, 'tag', 'ul'); - echo Html::beginTag($tag, $options) . "\n"; - echo $this->renderItems() . "\n"; - echo Html::endTag($tag) . "\n"; - $this->registerWidget('selectable', SelectableAsset::className()); - } + /** + * Renders selectable items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + public function renderItems() + { + $items = []; + foreach ($this->items as $item) { + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'li'); + if (is_array($item)) { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $options = array_merge($options, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', $tag); + $items[] = Html::tag($tag, $item['content'], $options); + } else { + $items[] = Html::tag($tag, $item, $options); + } + } - /** - * Renders selectable items as specified on [[items]]. - * @return string the rendering result. - * @throws InvalidConfigException. - */ - public function renderItems() - { - $items = []; - foreach ($this->items as $item) { - $options = $this->itemOptions; - $tag = ArrayHelper::remove($options, 'tag', 'li'); - if (is_array($item)) { - if (!isset($item['content'])) { - throw new InvalidConfigException("The 'content' option is required."); - } - $options = array_merge($options, ArrayHelper::getValue($item, 'options', [])); - $tag = ArrayHelper::remove($options, 'tag', $tag); - $items[] = Html::tag($tag, $item['content'], $options); - } else { - $items[] = Html::tag($tag, $item, $options); - } - } - return implode("\n", $items); - } + return implode("\n", $items); + } } diff --git a/extensions/jui/SelectableAsset.php b/extensions/jui/SelectableAsset.php index b794756357a..073be5207d5 100644 --- a/extensions/jui/SelectableAsset.php +++ b/extensions/jui/SelectableAsset.php @@ -15,11 +15,11 @@ */ class SelectableAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.selectable.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.selectable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/Slider.php b/extensions/jui/Slider.php index 5d80894937b..64c79e5505a 100644 --- a/extensions/jui/Slider.php +++ b/extensions/jui/Slider.php @@ -26,23 +26,23 @@ */ class Slider extends Widget { - /** - * @inheritDoc - */ - protected $clientEventMap = [ - 'change' => 'slidechange', - 'create' => 'slidecreate', - 'slide' => 'slide', - 'start' => 'slidestart', - 'stop' => 'slidestop', - ]; + /** + * @inheritDoc + */ + protected $clientEventMap = [ + 'change' => 'slidechange', + 'create' => 'slidecreate', + 'slide' => 'slide', + 'start' => 'slidestart', + 'stop' => 'slidestop', + ]; - /** - * Executes the widget. - */ - public function run() - { - echo Html::tag('div', '', $this->options); - $this->registerWidget('slider', SliderAsset::className()); - } + /** + * Executes the widget. + */ + public function run() + { + echo Html::tag('div', '', $this->options); + $this->registerWidget('slider', SliderAsset::className()); + } } diff --git a/extensions/jui/SliderAsset.php b/extensions/jui/SliderAsset.php index fcba7766d6f..96794a515c1 100644 --- a/extensions/jui/SliderAsset.php +++ b/extensions/jui/SliderAsset.php @@ -15,11 +15,11 @@ */ class SliderAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.slider.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.slider.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/SliderInput.php b/extensions/jui/SliderInput.php index b2383369f40..967a91ea46e 100644 --- a/extensions/jui/SliderInput.php +++ b/extensions/jui/SliderInput.php @@ -43,54 +43,54 @@ */ class SliderInput extends InputWidget { - /** - * @inheritDoc - */ - protected $clientEventMap = [ - 'change' => 'slidechange', - 'create' => 'slidecreate', - 'slide' => 'slide', - 'start' => 'slidestart', - 'stop' => 'slidestop', - ]; - /** - * @var array the HTML attributes for the container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $containerOptions = []; + /** + * @inheritDoc + */ + protected $clientEventMap = [ + 'change' => 'slidechange', + 'create' => 'slidecreate', + 'slide' => 'slide', + 'start' => 'slidestart', + 'stop' => 'slidestop', + ]; + /** + * @var array the HTML attributes for the container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $containerOptions = []; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (!isset($this->containerOptions['id'])) { - $this->containerOptions['id'] = $this->options['id'] . '-container'; - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!isset($this->containerOptions['id'])) { + $this->containerOptions['id'] = $this->options['id'] . '-container'; + } + } - /** - * Executes the widget. - */ - public function run() - { - echo Html::tag('div', '', $this->containerOptions); + /** + * Executes the widget. + */ + public function run() + { + echo Html::tag('div', '', $this->containerOptions); - if ($this->hasModel()) { - echo Html::activeHiddenInput($this->model, $this->attribute, $this->options); - $this->clientOptions['value'] = $this->model{$this->attribute}; - } else { - echo Html::hiddenInput($this->name, $this->value, $this->options); - $this->clientOptions['value'] = $this->value; - } + if ($this->hasModel()) { + echo Html::activeHiddenInput($this->model, $this->attribute, $this->options); + $this->clientOptions['value'] = $this->model{$this->attribute}; + } else { + echo Html::hiddenInput($this->name, $this->value, $this->options); + $this->clientOptions['value'] = $this->value; + } - if (!isset($this->clientEvents['slide'])) { - $this->clientEvents['slide'] = 'function(event, ui) { - $("#' . $this->options['id'] . '").val(ui.value); - }'; - } + if (!isset($this->clientEvents['slide'])) { + $this->clientEvents['slide'] = 'function (event, ui) { + $("#' . $this->options['id'] . '").val(ui.value); + }'; + } - $this->registerWidget('slider', SliderAsset::className(), $this->containerOptions['id']); - } + $this->registerWidget('slider', SliderAsset::className(), $this->containerOptions['id']); + } } diff --git a/extensions/jui/Sortable.php b/extensions/jui/Sortable.php index 19c93a9778a..a87cdf63475 100644 --- a/extensions/jui/Sortable.php +++ b/extensions/jui/Sortable.php @@ -38,92 +38,92 @@ */ class Sortable extends Widget { - /** - * @var array the HTML attributes for the widget container tag. The following special options are recognized: - * - * - tag: string, defaults to "ul", the tag name of the container tag of this widget. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array list of sortable items. Each item can be a string representing the item content - * or an array of the following structure: - * - * ~~~ - * [ - * 'content' => 'item content', - * // the HTML attributes of the item container tag. This will overwrite "itemOptions". - * 'options' => [], - * ] - * ~~~ - */ - public $items = []; - /** - * @var array list of HTML attributes for the item container tags. This will be overwritten - * by the "options" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "li", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the container tag of this widget. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array list of sortable items. Each item can be a string representing the item content + * or an array of the following structure: + * + * ~~~ + * [ + * 'content' => 'item content', + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => [], + * ] + * ~~~ + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; - /** - * @inheritDoc - */ - protected $clientEventMap = [ - 'activate' => 'sortactivate', - 'beforeStop' => 'sortbeforestop', - 'change' => 'sortchange', - 'create' => 'sortcreate', - 'deactivate' => 'sortdeactivate', - 'out' => 'sortout', - 'over' => 'sortover', - 'receive' => 'sortreceive', - 'remove' => 'sortremove', - 'sort' => 'sort', - 'start' => 'sortstart', - 'stop' => 'sortstop', - 'update' => 'sortupdate', - ]; + /** + * @inheritDoc + */ + protected $clientEventMap = [ + 'activate' => 'sortactivate', + 'beforeStop' => 'sortbeforestop', + 'change' => 'sortchange', + 'create' => 'sortcreate', + 'deactivate' => 'sortdeactivate', + 'out' => 'sortout', + 'over' => 'sortover', + 'receive' => 'sortreceive', + 'remove' => 'sortremove', + 'sort' => 'sort', + 'start' => 'sortstart', + 'stop' => 'sortstop', + 'update' => 'sortupdate', + ]; + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('sortable', SortableAsset::className()); + } - /** - * Renders the widget. - */ - public function run() - { - $options = $this->options; - $tag = ArrayHelper::remove($options, 'tag', 'ul'); - echo Html::beginTag($tag, $options) . "\n"; - echo $this->renderItems() . "\n"; - echo Html::endTag($tag) . "\n"; - $this->registerWidget('sortable', SortableAsset::className()); - } + /** + * Renders sortable items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + public function renderItems() + { + $items = []; + foreach ($this->items as $item) { + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'li'); + if (is_array($item)) { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $options = array_merge($options, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', $tag); + $items[] = Html::tag($tag, $item['content'], $options); + } else { + $items[] = Html::tag($tag, $item, $options); + } + } - /** - * Renders sortable items as specified on [[items]]. - * @return string the rendering result. - * @throws InvalidConfigException. - */ - public function renderItems() - { - $items = []; - foreach ($this->items as $item) { - $options = $this->itemOptions; - $tag = ArrayHelper::remove($options, 'tag', 'li'); - if (is_array($item)) { - if (!isset($item['content'])) { - throw new InvalidConfigException("The 'content' option is required."); - } - $options = array_merge($options, ArrayHelper::getValue($item, 'options', [])); - $tag = ArrayHelper::remove($options, 'tag', $tag); - $items[] = Html::tag($tag, $item['content'], $options); - } else { - $items[] = Html::tag($tag, $item, $options); - } - } - return implode("\n", $items); - } + return implode("\n", $items); + } } diff --git a/extensions/jui/SortableAsset.php b/extensions/jui/SortableAsset.php index 5f31f73447f..f7de366f2bb 100644 --- a/extensions/jui/SortableAsset.php +++ b/extensions/jui/SortableAsset.php @@ -15,11 +15,11 @@ */ class SortableAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.sortable.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.sortable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; } diff --git a/extensions/jui/Spinner.php b/extensions/jui/Spinner.php index c57c59aa59f..1222515d185 100644 --- a/extensions/jui/Spinner.php +++ b/extensions/jui/Spinner.php @@ -37,32 +37,32 @@ */ class Spinner extends InputWidget { - /** - * @inheritDoc - */ - protected $clientEventMap = [ - 'spin' => 'spin', - ]; + /** + * @inheritDoc + */ + protected $clientEventMap = [ + 'spin' => 'spin', + ]; - /** - * Renders the widget. - */ - public function run() - { - echo $this->renderWidget(); - $this->registerWidget('spinner', SpinnerAsset::className()); - } + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('spinner', SpinnerAsset::className()); + } - /** - * Renders the Spinner widget. - * @return string the rendering result. - */ - public function renderWidget() - { - if ($this->hasModel()) { - return Html::activeTextInput($this->model, $this->attribute, $this->options); - } else { - return Html::textInput($this->name, $this->value, $this->options); - } - } + /** + * Renders the Spinner widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } } diff --git a/extensions/jui/SpinnerAsset.php b/extensions/jui/SpinnerAsset.php index 04bc698bea6..8c36ef4c70b 100644 --- a/extensions/jui/SpinnerAsset.php +++ b/extensions/jui/SpinnerAsset.php @@ -15,12 +15,12 @@ */ class SpinnerAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.spinner.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\ButtonAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.spinner.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\ButtonAsset', + ]; } diff --git a/extensions/jui/Tabs.php b/extensions/jui/Tabs.php index 6d3b0f6b30a..4bff01e5c5d 100644 --- a/extensions/jui/Tabs.php +++ b/extensions/jui/Tabs.php @@ -53,99 +53,99 @@ */ class Tabs extends Widget { - /** - * @var array the HTML attributes for the widget container tag. The following special options are recognized: - * - * - tag: string, defaults to "div", the tag name of the container tag of this widget. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array list of tab items. Each item can be an array of the following structure: - * - * - label: string, required, specifies the header link label. When [[encodeLabels]] is true, the label - * will be HTML-encoded. - * - content: string, the content to show when corresponding tab is clicked. Can be omitted if url is specified. - * - url: mixed, mixed, optional, the url to load tab contents via AJAX. It is required if no content is specified. - * - template: string, optional, the header link template to render the header link. If none specified - * [[linkTemplate]] will be used instead. - * - options: array, optional, the HTML attributes of the header. - * - headerOptions: array, optional, the HTML attributes for the header container tag. - */ - public $items = []; - /** - * @var array list of HTML attributes for the item container tags. This will be overwritten - * by the "options" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "div", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; - /** - * @var array list of HTML attributes for the header container tags. This will be overwritten - * by the "headerOptions" set in individual [[items]]. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $headerOptions = []; - /** - * @var string the default header template to render the link. - */ - public $linkTemplate = '{label}'; - /** - * @var boolean whether the labels for header items should be HTML-encoded. - */ - public $encodeLabels = true; + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array list of tab items. Each item can be an array of the following structure: + * + * - label: string, required, specifies the header link label. When [[encodeLabels]] is true, the label + * will be HTML-encoded. + * - content: string, the content to show when corresponding tab is clicked. Can be omitted if url is specified. + * - url: mixed, mixed, optional, the url to load tab contents via AJAX. It is required if no content is specified. + * - template: string, optional, the header link template to render the header link. If none specified + * [[linkTemplate]] will be used instead. + * - options: array, optional, the HTML attributes of the header. + * - headerOptions: array, optional, the HTML attributes for the header container tag. + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * @var array list of HTML attributes for the header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var string the default header template to render the link. + */ + public $linkTemplate = '{label}'; + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('tabs', TabsAsset::className()); + } - /** - * Renders the widget. - */ - public function run() - { - $options = $this->options; - $tag = ArrayHelper::remove($options, 'tag', 'div'); - echo Html::beginTag($tag, $options) . "\n"; - echo $this->renderItems() . "\n"; - echo Html::endTag($tag) . "\n"; - $this->registerWidget('tabs', TabsAsset::className()); - } + /** + * Renders tab items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $headers = []; + $items = []; + foreach ($this->items as $n => $item) { + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + if (isset($item['url'])) { + $url = Url::to($item['url']); + } else { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' or 'url' option is required."); + } + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + if (!isset($options['id'])) { + $options['id'] = $this->options['id'] . '-tab' . $n; + } + $url = '#' . $options['id']; + $items[] = Html::tag($tag, $item['content'], $options); + } + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); + $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); + $headers[] = Html::tag('li', strtr($template, [ + '{label}' => $this->encodeLabels ? Html::encode($item['label']) : $item['label'], + '{url}' => $url, + ]), $headerOptions); + } - /** - * Renders tab items as specified on [[items]]. - * @return string the rendering result. - * @throws InvalidConfigException. - */ - protected function renderItems() - { - $headers = []; - $items = []; - foreach ($this->items as $n => $item) { - if (!isset($item['label'])) { - throw new InvalidConfigException("The 'label' option is required."); - } - if (isset($item['url'])) { - $url = Url::to($item['url']); - } else { - if (!isset($item['content'])) { - throw new InvalidConfigException("The 'content' or 'url' option is required."); - } - $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); - $tag = ArrayHelper::remove($options, 'tag', 'div'); - if (!isset($options['id'])) { - $options['id'] = $this->options['id'] . '-tab' . $n; - } - $url = '#' . $options['id']; - $items[] = Html::tag($tag, $item['content'], $options); - } - $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); - $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); - $headers[] = Html::tag('li', strtr($template, [ - '{label}' => $this->encodeLabels ? Html::encode($item['label']) : $item['label'], - '{url}' => $url, - ]), $headerOptions); - } - return Html::tag('ul', implode("\n", $headers)) . "\n" . implode("\n", $items); - } + return Html::tag('ul', implode("\n", $headers)) . "\n" . implode("\n", $items); + } } diff --git a/extensions/jui/TabsAsset.php b/extensions/jui/TabsAsset.php index 49b3bad10f7..0ae7f87bbe7 100644 --- a/extensions/jui/TabsAsset.php +++ b/extensions/jui/TabsAsset.php @@ -15,12 +15,12 @@ */ class TabsAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.tabs.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\EffectAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.tabs.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; } diff --git a/extensions/jui/ThemeAsset.php b/extensions/jui/ThemeAsset.php index 8c7e78e0388..bfb3abf81a6 100644 --- a/extensions/jui/ThemeAsset.php +++ b/extensions/jui/ThemeAsset.php @@ -15,8 +15,8 @@ */ class ThemeAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $css = [ - 'theme/jquery.ui.css', - ]; + public $sourcePath = '@yii/jui/assets'; + public $css = [ + 'theme/jquery.ui.css', + ]; } diff --git a/extensions/jui/TooltipAsset.php b/extensions/jui/TooltipAsset.php index f0139a1c0ce..6e2778a4e99 100644 --- a/extensions/jui/TooltipAsset.php +++ b/extensions/jui/TooltipAsset.php @@ -15,12 +15,12 @@ */ class TooltipAsset extends AssetBundle { - public $sourcePath = '@yii/jui/assets'; - public $js = [ - 'jquery.ui.tooltip.js', - ]; - public $depends = [ - 'yii\jui\CoreAsset', - 'yii\jui\EffectAsset', - ]; + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.tooltip.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; } diff --git a/extensions/jui/Widget.php b/extensions/jui/Widget.php index 6eb20f8806f..ff2c8fe5041 100644 --- a/extensions/jui/Widget.php +++ b/extensions/jui/Widget.php @@ -17,121 +17,120 @@ */ class Widget extends \yii\base\Widget { - /** - * @var string the jQuery UI theme. This refers to an asset bundle class - * representing the JUI theme. The default theme is the official "Smoothness" theme. - */ - public static $theme = 'yii\jui\ThemeAsset'; - /** - * @var array the HTML attributes for the widget container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array the options for the underlying jQuery UI widget. - * Please refer to the corresponding jQuery UI widget Web page for possible options. - * For example, [this page](http://api.jqueryui.com/accordion/) shows - * how to use the "Accordion" widget and the supported options (e.g. "header"). - */ - public $clientOptions = []; - /** - * @var array the event handlers for the underlying jQuery UI widget. - * Please refer to the corresponding jQuery UI widget Web page for possible events. - * For example, [this page](http://api.jqueryui.com/accordion/) shows - * how to use the "Accordion" widget and the supported events (e.g. "create"). - * Keys are the event names and values are javascript code that is passed to the `.on()` function - * as the event handler. - * - * For example you could write the following in your widget configuration: - * - * ```php - * 'clientEvents' => [ - * 'change' => 'function() { alert('event "change" occured.'); }' - * ], - * ``` - */ - public $clientEvents = []; + /** + * @var string the jQuery UI theme. This refers to an asset bundle class + * representing the JUI theme. The default theme is the official "Smoothness" theme. + */ + public static $theme = 'yii\jui\ThemeAsset'; + /** + * @var array the HTML attributes for the widget container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the options for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible options. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported options (e.g. "header"). + */ + public $clientOptions = []; + /** + * @var array the event handlers for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible events. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported events (e.g. "create"). + * Keys are the event names and values are javascript code that is passed to the `.on()` function + * as the event handler. + * + * For example you could write the following in your widget configuration: + * + * ```php + * 'clientEvents' => [ + * 'change' => 'function () { alert('event "change" occured.'); }' + * ], + * ``` + */ + public $clientEvents = []; - /** - * @var array event names mapped to what should be specified in .on( - * If empty, it is assumed that event passed to clientEvents is prefixed with widget name. - */ - protected $clientEventMap = []; + /** + * @var array event names mapped to what should be specified in .on( + * If empty, it is assumed that event passed to clientEvents is prefixed with widget name. + */ + protected $clientEventMap = []; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - parent::init(); - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - } + /** + * Registers a specific jQuery UI widget assets + * @param string $assetBundle the asset bundle for the widget + */ + protected function registerAssets($assetBundle) + { + /** @var \yii\web\AssetBundle $assetBundle */ + $assetBundle::register($this->getView()); + /** @var \yii\web\AssetBundle $themeAsset */ + $themeAsset = static::$theme; + $themeAsset::register($this->getView()); + } - /** - * Registers a specific jQuery UI widget assets - * @param string $assetBundle the asset bundle for the widget - */ - protected function registerAssets($assetBundle) - { - /** @var \yii\web\AssetBundle $assetBundle */ - $assetBundle::register($this->getView()); - /** @var \yii\web\AssetBundle $themeAsset */ - $themeAsset = static::$theme; - $themeAsset::register($this->getView()); - } + /** + * Registers a specific jQuery UI widget options + * @param string $name the name of the jQuery UI widget + * @param string $id the ID of the widget + */ + protected function registerClientOptions($name, $id) + { + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').$name($options);"; + $this->getView()->registerJs($js); + } + } - /** - * Registers a specific jQuery UI widget options - * @param string $name the name of the jQuery UI widget - * @param string $id the ID of the widget - */ - protected function registerClientOptions($name, $id) - { - if ($this->clientOptions !== false) { - $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); - $js = "jQuery('#$id').$name($options);"; - $this->getView()->registerJs($js); - } - } + /** + * Registers a specific jQuery UI widget events + * @param string $name the name of the jQuery UI widget + * @param string $id the ID of the widget + */ + protected function registerClientEvents($name, $id) + { + if (!empty($this->clientEvents)) { + $js = []; + foreach ($this->clientEvents as $event => $handler) { + if (isset($this->clientEventMap[$event])) { + $eventName = $this->clientEventMap[$event]; + } else { + $eventName = strtolower($name . $event); + } + $js[] = "jQuery('#$id').on('$eventName', $handler);"; + } + $this->getView()->registerJs(implode("\n", $js)); + } + } - /** - * Registers a specific jQuery UI widget events - * @param string $name the name of the jQuery UI widget - * @param string $id the ID of the widget - */ - protected function registerClientEvents($name, $id) - { - if (!empty($this->clientEvents)) { - $js = []; - foreach ($this->clientEvents as $event => $handler) { - if (isset($this->clientEventMap[$event])) { - $eventName = $this->clientEventMap[$event]; - } else { - $eventName = strtolower($name . $event); - } - $js[] = "jQuery('#$id').on('$eventName', $handler);"; - } - $this->getView()->registerJs(implode("\n", $js)); - } - } - - /** - * Registers a specific jQuery UI widget asset bundle, initializes it with client options and registers related events - * @param string $name the name of the jQuery UI widget - * @param string $assetBundle the asset bundle for the widget - * @param string $id the ID of the widget. If null, it will use the `id` value of [[options]]. - */ - protected function registerWidget($name, $assetBundle, $id = null) - { - if ($id === null) { - $id = $this->options['id']; - } - $this->registerAssets($assetBundle); - $this->registerClientOptions($name, $id); - $this->registerClientEvents($name, $id); - } + /** + * Registers a specific jQuery UI widget asset bundle, initializes it with client options and registers related events + * @param string $name the name of the jQuery UI widget + * @param string $assetBundle the asset bundle for the widget + * @param string $id the ID of the widget. If null, it will use the `id` value of [[options]]. + */ + protected function registerWidget($name, $assetBundle, $id = null) + { + if ($id === null) { + $id = $this->options['id']; + } + $this->registerAssets($assetBundle); + $this->registerClientOptions($name, $id); + $this->registerClientEvents($name, $id); + } } diff --git a/extensions/mongodb/ActiveFixture.php b/extensions/mongodb/ActiveFixture.php index 2acc5f4c207..3d3082aeeca 100644 --- a/extensions/mongodb/ActiveFixture.php +++ b/extensions/mongodb/ActiveFixture.php @@ -28,100 +28,101 @@ */ class ActiveFixture extends BaseActiveFixture { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - */ - public $db = 'mongodb'; - /** - * @var string|array the collection name that this fixture is about. If this property is not set, - * the table name will be determined via [[modelClass]]. - * @see Connection::getCollection() - */ - public $collectionName; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + */ + public $db = 'mongodb'; + /** + * @var string|array the collection name that this fixture is about. If this property is not set, + * the table name will be determined via [[modelClass]]. + * @see Connection::getCollection() + */ + public $collectionName; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (!isset($this->modelClass) && !isset($this->collectionName)) { - throw new InvalidConfigException('Either "modelClass" or "collectionName" must be set.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!isset($this->modelClass) && !isset($this->collectionName)) { + throw new InvalidConfigException('Either "modelClass" or "collectionName" must be set.'); + } + } - /** - * Loads the fixture data. - * Data will be batch inserted into the given collection. - */ - public function load() - { - parent::load(); + /** + * Loads the fixture data. + * Data will be batch inserted into the given collection. + */ + public function load() + { + parent::load(); - $data = $this->getData(); - $this->getCollection()->batchInsert($data); - foreach ($data as $alias => $row) { - $this->data[$alias] = $row; - } - } + $data = $this->getData(); + $this->getCollection()->batchInsert($data); + foreach ($data as $alias => $row) { + $this->data[$alias] = $row; + } + } - /** - * Unloads the fixture. - * - * The default implementation will clean up the colection by calling [[resetCollection()]]. - */ - public function unload() - { - $this->resetCollection(); - parent::unload(); - } + /** + * Unloads the fixture. + * + * The default implementation will clean up the colection by calling [[resetCollection()]]. + */ + public function unload() + { + $this->resetCollection(); + parent::unload(); + } - protected function getCollection() - { - return $this->db->getCollection($this->getCollectionName()); - } + protected function getCollection() + { + return $this->db->getCollection($this->getCollectionName()); + } - protected function getCollectionName() - { - if ($this->collectionName) { - return $this->collectionName; - } else { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - return $modelClass::collectionName(); - } - } + protected function getCollectionName() + { + if ($this->collectionName) { + return $this->collectionName; + } else { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; - /** - * Returns the fixture data. - * - * This method is called by [[loadData()]] to get the needed fixture data. - * - * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. - * The file should return an array of data rows (column name => column value), each corresponding to a row in the table. - * - * If the data file does not exist, an empty array will be returned. - * - * @return array the data rows to be inserted into the collection. - */ - protected function getData() - { - if ($this->dataFile === null) { - $class = new \ReflectionClass($this); - $dataFile = dirname($class->getFileName()) . '/data/' . $this->getCollectionName() . '.php'; - return is_file($dataFile) ? require($dataFile) : []; - } else { - return parent::getData(); - } - } + return $modelClass::collectionName(); + } + } - /** - * Removes all existing data from the specified collection and resets sequence number if any. - * This method is called before populating fixture data into the collection associated with this fixture. - */ - protected function resetCollection() - { - $this->getCollection()->remove(); - } + /** + * Returns the fixture data. + * + * This method is called by [[loadData()]] to get the needed fixture data. + * + * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. + * The file should return an array of data rows (column name => column value), each corresponding to a row in the table. + * + * If the data file does not exist, an empty array will be returned. + * + * @return array the data rows to be inserted into the collection. + */ + protected function getData() + { + if ($this->dataFile === null) { + $class = new \ReflectionClass($this); + $dataFile = dirname($class->getFileName()) . '/data/' . $this->getCollectionName() . '.php'; + + return is_file($dataFile) ? require($dataFile) : []; + } else { + return parent::getData(); + } + } + + /** + * Removes all existing data from the specified collection and resets sequence number if any. + * This method is called before populating fixture data into the collection associated with this fixture. + */ + protected function resetCollection() + { + $this->getCollection()->remove(); + } } diff --git a/extensions/mongodb/ActiveQuery.php b/extensions/mongodb/ActiveQuery.php index fabea5f7429..95a242c9173 100644 --- a/extensions/mongodb/ActiveQuery.php +++ b/extensions/mongodb/ActiveQuery.php @@ -61,115 +61,119 @@ */ class ActiveQuery extends Query implements ActiveQueryInterface { - use ActiveQueryTrait; - use ActiveRelationTrait; + use ActiveQueryTrait; + use ActiveRelationTrait; - /** - * @inheritdoc - */ - protected function buildCursor($db = null) - { - if ($this->primaryModel !== null) { - // lazy loading - if ($this->via instanceof self) { - // via pivot collection - $viaModels = $this->via->findPivotRows([$this->primaryModel]); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveQuery $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - } - return parent::buildCursor($db); - } + /** + * @inheritdoc + */ + protected function buildCursor($db = null) + { + if ($this->primaryModel !== null) { + // lazy loading + if ($this->via instanceof self) { + // via pivot collection + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveQuery $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); + } else { + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; + } + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); + } + } - /** - * Executes query and returns all results as an array. - * @param Connection $db the Mongo connection used to execute the query. - * If null, the Mongo connection returned by [[modelClass]] will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $cursor = $this->buildCursor($db); - $rows = $this->fetchRows($cursor); - if (!empty($rows)) { - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - return $models; - } else { - return []; - } - } + return parent::buildCursor($db); + } - /** - * Executes query and returns a single row of result. - * @param Connection $db the Mongo connection used to execute the query. - * If null, the Mongo connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - $row = parent::one($db); - if ($row !== false) { - if ($this->asArray) { - $model = $row; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - if (!$this->asArray) { - $model->afterFind(); - } - return $model; - } else { - return null; - } - } + /** + * Executes query and returns all results as an array. + * @param Connection $db the Mongo connection used to execute the query. + * If null, the Mongo connection returned by [[modelClass]] will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $cursor = $this->buildCursor($db); + $rows = $this->fetchRows($cursor); + if (!empty($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } - /** - * Returns the Mongo collection for this query. - * @param Connection $db Mongo connection. - * @return Collection collection instance. - */ - public function getCollection($db = null) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } - if ($this->from === null) { - $this->from = $modelClass::collectionName(); - } - return $db->getCollection($this->from); - } + return $models; + } else { + return []; + } + } + + /** + * Executes query and returns a single row of result. + * @param Connection $db the Mongo connection used to execute the query. + * If null, the Mongo connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + $row = parent::one($db); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + if (!$this->asArray) { + $model->afterFind(); + } + + return $model; + } else { + return null; + } + } + + /** + * Returns the Mongo collection for this query. + * @param Connection $db Mongo connection. + * @return Collection collection instance. + */ + public function getCollection($db = null) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } + if ($this->from === null) { + $this->from = $modelClass::collectionName(); + } + + return $db->getCollection($this->from); + } } diff --git a/extensions/mongodb/ActiveRecord.php b/extensions/mongodb/ActiveRecord.php index fdf73011e44..ebeda89f4cb 100644 --- a/extensions/mongodb/ActiveRecord.php +++ b/extensions/mongodb/ActiveRecord.php @@ -21,331 +21,339 @@ */ abstract class ActiveRecord extends BaseActiveRecord { - /** - * Returns the Mongo connection used by this AR class. - * By default, the "mongodb" application component is used as the Mongo connection. - * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->getComponent('mongodb'); - } + /** + * Returns the Mongo connection used by this AR class. + * By default, the "mongodb" application component is used as the Mongo connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getComponent('mongodb'); + } - /** - * Updates all documents in the collection using the provided attribute values and conditions. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], ['status' => 2]); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the collection - * @param array $condition description of the objects to update. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $options list of options in format: optionName => optionValue. - * @return integer the number of documents updated. - */ - public static function updateAll($attributes, $condition = [], $options = []) - { - return static::getCollection()->update($condition, $attributes, $options); - } + /** + * Updates all documents in the collection using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], ['status' => 2]); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the collection + * @param array $condition description of the objects to update. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $options list of options in format: optionName => optionValue. + * @return integer the number of documents updated. + */ + public static function updateAll($attributes, $condition = [], $options = []) + { + return static::getCollection()->update($condition, $attributes, $options); + } - /** - * Updates all documents in the collection using the provided counter changes and conditions. - * For example, to increment all customers' age by 1, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param array $condition description of the objects to update. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $options list of options in format: optionName => optionValue. - * @return integer the number of documents updated. - */ - public static function updateAllCounters($counters, $condition = [], $options = []) - { - return static::getCollection()->update($condition, ['$inc' => $counters], $options); - } + /** + * Updates all documents in the collection using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param array $condition description of the objects to update. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $options list of options in format: optionName => optionValue. + * @return integer the number of documents updated. + */ + public static function updateAllCounters($counters, $condition = [], $options = []) + { + return static::getCollection()->update($condition, ['$inc' => $counters], $options); + } - /** - * Deletes documents in the collection using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete documents rows in the collection. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll(['status' => 3]); - * ~~~ - * - * @param array $condition description of the objects to delete. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $options list of options in format: optionName => optionValue. - * @return integer the number of documents deleted. - */ - public static function deleteAll($condition = [], $options = []) - { - return static::getCollection()->remove($condition, $options); - } + /** + * Deletes documents in the collection using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete documents rows in the collection. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll(['status' => 3]); + * ~~~ + * + * @param array $condition description of the objects to delete. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $options list of options in format: optionName => optionValue. + * @return integer the number of documents deleted. + */ + public static function deleteAll($condition = [], $options = []) + { + return static::getCollection()->remove($condition, $options); + } - /** - * Creates an [[ActiveQuery]] instance. - * - * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also - * by [[hasOne()]] and [[hasMany()]] to create a relational query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ActiveQuery($config); - } + /** + * Creates an [[ActiveQuery]] instance. + * + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also + * by [[hasOne()]] and [[hasMany()]] to create a relational query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); - /** - * Declares the name of the Mongo collection associated with this AR class. - * Collection name can be either a string or array: - * - if string considered as the name of the collection inside the default database. - * - if array - first element considered as the name of the database, second - as - * name of collection inside that database - * By default this method returns the class name as the collection name by calling [[Inflector::camel2id()]]. - * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes - * 'order_item'. You may override this method if the table is not named after this convention. - * @return string|array the collection name - */ - public static function collectionName() - { - return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); - } + return new ActiveQuery($config); + } - /** - * Return the Mongo collection instance for this AR class. - * @return Collection collection instance. - */ - public static function getCollection() - { - return static::getDb()->getCollection(static::collectionName()); - } + /** + * Declares the name of the Mongo collection associated with this AR class. + * Collection name can be either a string or array: + * - if string considered as the name of the collection inside the default database. + * - if array - first element considered as the name of the database, second - as + * name of collection inside that database + * By default this method returns the class name as the collection name by calling [[Inflector::camel2id()]]. + * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes + * 'order_item'. You may override this method if the table is not named after this convention. + * @return string|array the collection name + */ + public static function collectionName() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); + } - /** - * Returns the primary key name(s) for this AR class. - * The default implementation will return ['_id']. - * - * Note that an array should be returned even for a collection with single primary key. - * - * @return string[] the primary keys of the associated Mongo collection. - */ - public static function primaryKey() - { - return ['_id']; - } + /** + * Return the Mongo collection instance for this AR class. + * @return Collection collection instance. + */ + public static function getCollection() + { + return static::getDb()->getCollection(static::collectionName()); + } - /** - * Returns the list of all attribute names of the model. - * This method must be overridden by child classes to define available attributes. - * Note: primary key attribute "_id" should be always present in returned array. - * For example: - * ~~~ - * public function attributes() - * { - * return ['_id', 'name', 'address', 'status']; - * } - * ~~~ - * @return array list of attribute names. - */ - public function attributes() - { - throw new InvalidConfigException('The attributes() method of mongodb ActiveRecord has to be implemented by child classes.'); - } + /** + * Returns the primary key name(s) for this AR class. + * The default implementation will return ['_id']. + * + * Note that an array should be returned even for a collection with single primary key. + * + * @return string[] the primary keys of the associated Mongo collection. + */ + public static function primaryKey() + { + return ['_id']; + } - /** - * Inserts a row into the associated Mongo collection using the attribute values of this record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. insert the record into collection. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. - * - * If the primary key is null during insertion, it will be populated with the actual - * value after insertion. - * - * For example, to insert a customer record: - * - * ~~~ - * $customer = new Customer; - * $customer->name = $name; - * $customer->email = $email; - * $customer->insert(); - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the collection. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded will be saved. - * @return boolean whether the attributes are valid and the record is inserted successfully. - * @throws \Exception in case insert failed. - */ - public function insert($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - $result = $this->insertInternal($attributes); - return $result; - } + /** + * Returns the list of all attribute names of the model. + * This method must be overridden by child classes to define available attributes. + * Note: primary key attribute "_id" should be always present in returned array. + * For example: + * ~~~ + * public function attributes() + * { + * return ['_id', 'name', 'address', 'status']; + * } + * ~~~ + * @return array list of attribute names. + */ + public function attributes() + { + throw new InvalidConfigException('The attributes() method of mongodb ActiveRecord has to be implemented by child classes.'); + } - /** - * @see ActiveRecord::insert() - */ - protected function insertInternal($attributes = null) - { - if (!$this->beforeSave(true)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $currentAttributes = $this->getAttributes(); - foreach ($this->primaryKey() as $key) { - $values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null; - } - } - $newId = static::getCollection()->insert($values); - $this->setAttribute('_id', $newId); - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $value); - } - $this->afterSave(true); - return true; - } + /** + * Inserts a row into the associated Mongo collection using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into collection. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. + * + * If the primary key is null during insertion, it will be populated with the actual + * value after insertion. + * + * For example, to insert a customer record: + * + * ~~~ + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the collection. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws \Exception in case insert failed. + */ + public function insert($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $result = $this->insertInternal($attributes); - /** - * @see ActiveRecord::update() - * @throws StaleObjectException - */ - protected function updateInternal($attributes = null) - { - if (!$this->beforeSave(false)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $this->afterSave(false); - return 0; - } - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - if (!isset($values[$lock])) { - $values[$lock] = $this->$lock + 1; - } - $condition[$lock] = $this->$lock; - } - // We do not check the return value of update() because it's possible - // that it doesn't change anything and thus returns 0. - $rows = static::getCollection()->update($condition, $values); + return $result; + } - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); - } + /** + * @see ActiveRecord::insert() + */ + protected function insertInternal($attributes = null) + { + if (!$this->beforeSave(true)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $currentAttributes = $this->getAttributes(); + foreach ($this->primaryKey() as $key) { + $values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null; + } + } + $newId = static::getCollection()->insert($values); + $this->setAttribute('_id', $newId); + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $value); + } + $this->afterSave(true); - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $this->getAttribute($name)); - } - $this->afterSave(false); - return $rows; - } + return true; + } - /** - * Deletes the document corresponding to this active record from the collection. - * - * This method performs the following steps in order: - * - * 1. call [[beforeDelete()]]. If the method returns false, it will skip the - * rest of the steps; - * 2. delete the document from the collection; - * 3. call [[afterDelete()]]. - * - * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] - * will be raised by the corresponding methods. - * - * @return integer|boolean the number of documents deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible the number of documents deleted is 0, even though the deletion execution is successful. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being deleted is outdated. - * @throws \Exception in case delete failed. - */ - public function delete() - { - $result = false; - if ($this->beforeDelete()) { - $result = $this->deleteInternal(); - $this->afterDelete(); - } - return $result; - } + /** + * @see ActiveRecord::update() + * @throws StaleObjectException + */ + protected function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false); - /** - * @see ActiveRecord::delete() - * @throws StaleObjectException - */ - protected function deleteInternal() - { - // we do not check the return value of deleteAll() because it's possible - // the record is already deleted in the database and thus the method will return 0 - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - $condition[$lock] = $this->$lock; - } - $result = static::getCollection()->remove($condition); - if ($lock !== null && !$result) { - throw new StaleObjectException('The object being deleted is outdated.'); - } - $this->setOldAttributes(null); - return $result; - } + return 0; + } + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } + // We do not check the return value of update() because it's possible + // that it doesn't change anything and thus returns 0. + $rows = static::getCollection()->update($condition, $values); - /** - * Returns a value indicating whether the given active record is the same as the current one. - * The comparison is made by comparing the table names and the primary key values of the two active records. - * If one of the records [[isNewRecord|is new]] they are also considered not equal. - * @param ActiveRecord $record record to compare to - * @return boolean whether the two active records refer to the same row in the same Mongo collection. - */ - public function equals($record) - { - if ($this->isNewRecord || $record->isNewRecord) { - return false; - } - return $this->collectionName() === $record->collectionName() && (string)$this->getPrimaryKey() === (string)$record->getPrimaryKey(); - } + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $this->getAttribute($name)); + } + $this->afterSave(false); + + return $rows; + } + + /** + * Deletes the document corresponding to this active record from the collection. + * + * This method performs the following steps in order: + * + * 1. call [[beforeDelete()]]. If the method returns false, it will skip the + * rest of the steps; + * 2. delete the document from the collection; + * 3. call [[afterDelete()]]. + * + * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] + * will be raised by the corresponding methods. + * + * @return integer|boolean the number of documents deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of documents deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. + * @throws \Exception in case delete failed. + */ + public function delete() + { + $result = false; + if ($this->beforeDelete()) { + $result = $this->deleteInternal(); + $this->afterDelete(); + } + + return $result; + } + + /** + * @see ActiveRecord::delete() + * @throws StaleObjectException + */ + protected function deleteInternal() + { + // we do not check the return value of deleteAll() because it's possible + // the record is already deleted in the database and thus the method will return 0 + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = static::getCollection()->remove($condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->setOldAttributes(null); + + return $result; + } + + /** + * Returns a value indicating whether the given active record is the same as the current one. + * The comparison is made by comparing the table names and the primary key values of the two active records. + * If one of the records [[isNewRecord|is new]] they are also considered not equal. + * @param ActiveRecord $record record to compare to + * @return boolean whether the two active records refer to the same row in the same Mongo collection. + */ + public function equals($record) + { + if ($this->isNewRecord || $record->isNewRecord) { + return false; + } + + return $this->collectionName() === $record->collectionName() && (string) $this->getPrimaryKey() === (string) $record->getPrimaryKey(); + } } diff --git a/extensions/mongodb/Cache.php b/extensions/mongodb/Cache.php index af016a51c20..7f4edce6c86 100644 --- a/extensions/mongodb/Cache.php +++ b/extensions/mongodb/Cache.php @@ -34,169 +34,173 @@ */ class Cache extends \yii\caching\Cache { - /** - * @var Connection|string the MongoDB connection object or the application component ID of the MongoDB connection. - * After the Cache object is created, if you want to change this property, you should only assign it - * with a MongoDB connection object. - */ - public $db = 'mongodb'; - /** - * @var string|array the name of the MongoDB collection that stores the cache data. - * Please refer to [[Connection::getCollection()]] on how to specify this parameter. - * This collection is better to be pre-created with fields 'id' and 'expire' indexed. - */ - public $cacheCollection = 'cache'; - /** - * @var integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. - * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. - */ - public $gcProbability = 100; - - /** - * Initializes the Cache component. - * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException($this->className() . "::db must be either a MongoDB connection instance or the application component ID of a MongoDB connection."); - } - } - - /** - * Retrieves a value from cache with a specified key. - * This method should be implemented by child classes to retrieve the data - * from specific cache storage. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - $query = new Query; - $row = $query->select(['data']) - ->from($this->cacheCollection) - ->where([ - 'id' => $key, - '$or' => [ - [ - 'expire' => 0 - ], - [ - 'expire' => ['$gt' => time()] - ], - ], - ]) - ->one($this->db); - if (empty($row)) { - return false; - } else { - return $row['data']; - } - } - - /** - * Stores a value identified by a key in cache. - * This method should be implemented by child classes to store the data - * in specific cache storage. - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - $result = $this->db->getCollection($this->cacheCollection) - ->update(['id' => $key], [ - 'expire' => $expire > 0 ? $expire + time() : 0, - 'data' => $value, - ]); - - if ($result) { - $this->gc(); - return true; - } else { - return $this->addValue($key, $value, $expire); - } - } - - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This method should be implemented by child classes to store the data - * in specific cache storage. - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - $this->gc(); - - if ($expire > 0) { - $expire += time(); - } else { - $expire = 0; - } - - try { - $this->db->getCollection($this->cacheCollection) - ->insert([ - 'id' => $key, - 'expire' => $expire, - 'data' => $value, - ]); - return true; - } catch (\Exception $e) { - return false; - } - } - - /** - * Deletes a value with the specified key from cache - * This method should be implemented by child classes to delete the data from actual cache storage. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - $this->db->getCollection($this->cacheCollection) - ->remove(['id' => $key]); - return true; - } - - /** - * Deletes all values from cache. - * Child classes may implement this method to realize the flush operation. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - $this->db->getCollection($this->cacheCollection) - ->remove(); - return true; - } - - /** - * Removes the expired data values. - * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. - * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. - */ - public function gc($force = false) - { - if ($force || mt_rand(0, 1000000) < $this->gcProbability) { - $this->db->getCollection($this->cacheCollection) - ->remove([ - 'expire' => [ - '$gt' => 0, - '$lt' => time(), - ] - ]); - } - } + /** + * @var Connection|string the MongoDB connection object or the application component ID of the MongoDB connection. + * After the Cache object is created, if you want to change this property, you should only assign it + * with a MongoDB connection object. + */ + public $db = 'mongodb'; + /** + * @var string|array the name of the MongoDB collection that stores the cache data. + * Please refer to [[Connection::getCollection()]] on how to specify this parameter. + * This collection is better to be pre-created with fields 'id' and 'expire' indexed. + */ + public $cacheCollection = 'cache'; + /** + * @var integer the probability (parts per million) that garbage collection (GC) should be performed + * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. + * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. + */ + public $gcProbability = 100; + + /** + * Initializes the Cache component. + * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException($this->className() . "::db must be either a MongoDB connection instance or the application component ID of a MongoDB connection."); + } + } + + /** + * Retrieves a value from cache with a specified key. + * This method should be implemented by child classes to retrieve the data + * from specific cache storage. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + $query = new Query; + $row = $query->select(['data']) + ->from($this->cacheCollection) + ->where([ + 'id' => $key, + '$or' => [ + [ + 'expire' => 0 + ], + [ + 'expire' => ['$gt' => time()] + ], + ], + ]) + ->one($this->db); + if (empty($row)) { + return false; + } else { + return $row['data']; + } + } + + /** + * Stores a value identified by a key in cache. + * This method should be implemented by child classes to store the data + * in specific cache storage. + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + $result = $this->db->getCollection($this->cacheCollection) + ->update(['id' => $key], [ + 'expire' => $expire > 0 ? $expire + time() : 0, + 'data' => $value, + ]); + + if ($result) { + $this->gc(); + + return true; + } else { + return $this->addValue($key, $value, $expire); + } + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This method should be implemented by child classes to store the data + * in specific cache storage. + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + $this->gc(); + + if ($expire > 0) { + $expire += time(); + } else { + $expire = 0; + } + + try { + $this->db->getCollection($this->cacheCollection) + ->insert([ + 'id' => $key, + 'expire' => $expire, + 'data' => $value, + ]); + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * Deletes a value with the specified key from cache + * This method should be implemented by child classes to delete the data from actual cache storage. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + $this->db->getCollection($this->cacheCollection) + ->remove(['id' => $key]); + + return true; + } + + /** + * Deletes all values from cache. + * Child classes may implement this method to realize the flush operation. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + $this->db->getCollection($this->cacheCollection) + ->remove(); + + return true; + } + + /** + * Removes the expired data values. + * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. + * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. + */ + public function gc($force = false) + { + if ($force || mt_rand(0, 1000000) < $this->gcProbability) { + $this->db->getCollection($this->cacheCollection) + ->remove([ + 'expire' => [ + '$gt' => 0, + '$lt' => time(), + ] + ]); + } + } } diff --git a/extensions/mongodb/Collection.php b/extensions/mongodb/Collection.php index 3a5a9108490..237fb086959 100644 --- a/extensions/mongodb/Collection.php +++ b/extensions/mongodb/Collection.php @@ -68,932 +68,954 @@ */ class Collection extends Object { - /** - * @var \MongoCollection Mongo collection instance. - */ - public $mongoCollection; - - /** - * @return string name of this collection. - */ - public function getName() - { - return $this->mongoCollection->getName(); - } - - /** - * @return string full name of this collection, including database name. - */ - public function getFullName() - { - return $this->mongoCollection->__toString(); - } - - /** - * @return array last error information. - */ - public function getLastError() - { - return $this->mongoCollection->db->lastError(); - } - - /** - * Composes log/profile token. - * @param string $command command name - * @param array $arguments command arguments. - * @return string token. - */ - protected function composeLogToken($command, $arguments = []) - { - $parts = []; - foreach ($arguments as $argument) { - $parts[] = is_scalar($argument) ? $argument : $this->encodeLogData($argument); - } - return $this->getFullName() . '.' . $command . '(' . implode(', ', $parts) . ')'; - } - - /** - * Encodes complex log data into JSON format string. - * @param mixed $data raw data. - * @return string encoded data string. - */ - protected function encodeLogData($data) - { - return json_encode($this->processLogData($data)); - } - - /** - * Pre-processes the log data before sending it to `json_encode()`. - * @param mixed $data raw data. - * @return mixed the processed data. - */ - protected function processLogData($data) - { - if (is_object($data)) { - if ($data instanceof \MongoId || - $data instanceof \MongoRegex || - $data instanceof \MongoDate || - $data instanceof \MongoInt32 || - $data instanceof \MongoInt64 || - $data instanceof \MongoTimestamp - ) { - $data = get_class($data) . '(' . $data->__toString() . ')'; - } elseif ($data instanceof \MongoCode) { - $data = 'MongoCode( ' . $data->__toString() . ' )'; - } elseif ($data instanceof \MongoBinData) { - $data = 'MongoBinData(...)'; - } elseif ($data instanceof \MongoDBRef) { - $data = 'MongoDBRef(...)'; - } elseif ($data instanceof \MongoMinKey || $data instanceof \MongoMaxKey) { - $data = get_class($data); - } else { - $result = []; - foreach ($data as $name => $value) { - $result[$name] = $value; - } - $data = $result; - } - - if ($data === []) { - return new \stdClass(); - } - } - - if (is_array($data)) { - foreach ($data as $key => $value) { - if (is_array($value) || is_object($value)) { - $data[$key] = $this->processLogData($value); - } - } - } - - return $data; - } - - /** - * Drops this collection. - * @throws Exception on failure. - * @return boolean whether the operation successful. - */ - public function drop() - { - $token = $this->composeLogToken('drop'); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->drop(); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return true; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Creates an index on the collection and the specified fields. - * @param array|string $columns column name or list of column names. - * If array is given, each element in the array has as key the field name, and as - * value either 1 for ascending sort, or -1 for descending sort. - * You can specify field using native numeric key with the field name as a value, - * in this case ascending sort will be used. - * For example: - * ~~~ - * [ - * 'name', - * 'status' => -1, - * ] - * ~~~ - * @param array $options list of options in format: optionName => optionValue. - * @throws Exception on failure. - * @return boolean whether the operation successful. - */ - public function createIndex($columns, $options = []) - { - if (!is_array($columns)) { - $columns = [$columns]; - } - $keys = $this->normalizeIndexKeys($columns); - $token = $this->composeLogToken('createIndex', [$keys, $options]); - $options = array_merge(['w' => 1], $options); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->ensureIndex($keys, $options); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return true; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Drop indexes for specified column(s). - * @param string|array $columns column name or list of column names. - * If array is given, each element in the array has as key the field name, and as - * value either 1 for ascending sort, or -1 for descending sort. - * Use value 'text' to specify text index. - * You can specify field using native numeric key with the field name as a value, - * in this case ascending sort will be used. - * For example: - * ~~~ - * [ - * 'name', - * 'status' => -1, - * 'description' => 'text', - * ] - * ~~~ - * @throws Exception on failure. - * @return boolean whether the operation successful. - */ - public function dropIndex($columns) - { - if (!is_array($columns)) { - $columns = [$columns]; - } - $keys = $this->normalizeIndexKeys($columns); - $token = $this->composeLogToken('dropIndex', [$keys]); - Yii::info($token, __METHOD__); - try { - $result = $this->mongoCollection->deleteIndex($keys); - $this->tryResultError($result); - return true; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Compose index keys from given columns/keys list. - * @param array $columns raw columns/keys list. - * @return array normalizes index keys array. - */ - protected function normalizeIndexKeys($columns) - { - $keys = []; - foreach ($columns as $key => $value) { - if (is_numeric($key)) { - $keys[$value] = \MongoCollection::ASCENDING; - } else { - $keys[$key] = $value; - } - } - return $keys; - } - - /** - * Drops all indexes for this collection. - * @throws Exception on failure. - * @return integer count of dropped indexes. - */ - public function dropAllIndexes() - { - $token = $this->composeLogToken('dropIndexes'); - Yii::info($token, __METHOD__); - try { - $result = $this->mongoCollection->deleteIndexes(); - $this->tryResultError($result); - return $result['nIndexesWas']; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Returns a cursor for the search results. - * In order to perform "find" queries use [[Query]] class. - * @param array $condition query condition - * @param array $fields fields to be selected - * @return \MongoCursor cursor for the search results - * @see Query - */ - public function find($condition = [], $fields = []) - { - return $this->mongoCollection->find($this->buildCondition($condition), $fields); - } - - /** - * Returns a single document. - * @param array $condition query condition - * @param array $fields fields to be selected - * @return array|null the single document. Null is returned if the query results in nothing. - * @see http://www.php.net/manual/en/mongocollection.findone.php - */ - public function findOne($condition = [], $fields = []) - { - return $this->mongoCollection->findOne($this->buildCondition($condition), $fields); - } - - /** - * Updates a document and returns it. - * @param array $condition query condition - * @param array $update update criteria - * @param array $fields fields to be returned - * @param array $options list of options in format: optionName => optionValue. - * @return array|null the original document, or the modified document when $options['new'] is set. - * @throws Exception on failure. - * @see http://www.php.net/manual/en/mongocollection.findandmodify.php - */ - public function findAndModify($condition, $update, $fields = [], $options = []) - { - $condition = $this->buildCondition($condition); - $token = $this->composeLogToken('findAndModify', [$condition, $update, $fields, $options]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->findAndModify($condition, $update, $fields, $options); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Inserts new data into collection. - * @param array|object $data data to be inserted. - * @param array $options list of options in format: optionName => optionValue. - * @return \MongoId new record id instance. - * @throws Exception on failure. - */ - public function insert($data, $options = []) - { - $token = $this->composeLogToken('insert', [$data]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $options = array_merge(['w' => 1], $options); - $this->tryResultError($this->mongoCollection->insert($data, $options)); - Yii::endProfile($token, __METHOD__); - return is_array($data) ? $data['_id'] : $data->_id; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Inserts several new rows into collection. - * @param array $rows array of arrays or objects to be inserted. - * @param array $options list of options in format: optionName => optionValue. - * @return array inserted data, each row will have "_id" key assigned to it. - * @throws Exception on failure. - */ - public function batchInsert($rows, $options = []) - { - $token = $this->composeLogToken('batchInsert', [$rows]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $options = array_merge(['w' => 1], $options); - $this->tryResultError($this->mongoCollection->batchInsert($rows, $options)); - Yii::endProfile($token, __METHOD__); - return $rows; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Updates the rows, which matches given criteria by given data. - * Note: for "multiple" mode Mongo requires explicit strategy "$set" or "$inc" - * to be specified for the "newData". If no strategy is passed "$set" will be used. - * @param array $condition description of the objects to update. - * @param array $newData the object with which to update the matching records. - * @param array $options list of options in format: optionName => optionValue. - * @return integer|boolean number of updated documents or whether operation was successful. - * @throws Exception on failure. - */ - public function update($condition, $newData, $options = []) - { - $condition = $this->buildCondition($condition); - $options = array_merge(['w' => 1, 'multiple' => true], $options); - if ($options['multiple']) { - $keys = array_keys($newData); - if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) { - $newData = ['$set' => $newData]; - } - } - $token = $this->composeLogToken('update', [$condition, $newData, $options]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->update($condition, $newData, $options); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - if (is_array($result) && array_key_exists('n', $result)) { - return $result['n']; - } else { - return true; - } - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Update the existing database data, otherwise insert this data - * @param array|object $data data to be updated/inserted. - * @param array $options list of options in format: optionName => optionValue. - * @return \MongoId updated/new record id instance. - * @throws Exception on failure. - */ - public function save($data, $options = []) - { - $token = $this->composeLogToken('save', [$data]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $options = array_merge(['w' => 1], $options); - $this->tryResultError($this->mongoCollection->save($data, $options)); - Yii::endProfile($token, __METHOD__); - return is_array($data) ? $data['_id'] : $data->_id; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Removes data from the collection. - * @param array $condition description of records to remove. - * @param array $options list of options in format: optionName => optionValue. - * @return integer|boolean number of updated documents or whether operation was successful. - * @throws Exception on failure. - * @see http://www.php.net/manual/en/mongocollection.remove.php - */ - public function remove($condition = [], $options = []) - { - $condition = $this->buildCondition($condition); - $options = array_merge(['w' => 1, 'justOne' => false], $options); - $token = $this->composeLogToken('remove', [$condition, $options]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->remove($condition, $options); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - if (is_array($result) && array_key_exists('n', $result)) { - return $result['n']; - } else { - return true; - } - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Returns a list of distinct values for the given column across a collection. - * @param string $column column to use. - * @param array $condition query parameters. - * @return array|boolean array of distinct values, or "false" on failure. - * @throws Exception on failure. - */ - public function distinct($column, $condition = []) - { - $condition = $this->buildCondition($condition); - $token = $this->composeLogToken('distinct', [$column, $condition]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->distinct($column, $condition); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Performs aggregation using Mongo Aggregation Framework. - * @param array $pipeline list of pipeline operators, or just the first operator - * @param array $pipelineOperator additional pipeline operator. You can specify additional - * pipelines via third argument, fourth argument etc. - * @return array the result of the aggregation. - * @throws Exception on failure. - * @see http://docs.mongodb.org/manual/applications/aggregation/ - */ - public function aggregate($pipeline, $pipelineOperator = []) - { - $args = func_get_args(); - $token = $this->composeLogToken('aggregate', $args); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = call_user_func_array([$this->mongoCollection, 'aggregate'], $args); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return $result['result']; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Performs aggregation using Mongo "group" command. - * @param mixed $keys fields to group by. If an array or non-code object is passed, - * it will be the key used to group results. If instance of [[\MongoCode]] passed, - * it will be treated as a function that returns the key to group by. - * @param array $initial Initial value of the aggregation counter object. - * @param \MongoCode|string $reduce function that takes two arguments (the current - * document and the aggregation to this point) and does the aggregation. - * Argument will be automatically cast to [[\MongoCode]]. - * @param array $options optional parameters to the group command. Valid options include: - * - condition - criteria for including a document in the aggregation. - * - finalize - function called once per unique key that takes the final output of the reduce function. - * @return array the result of the aggregation. - * @throws Exception on failure. - * @see http://docs.mongodb.org/manual/reference/command/group/ - */ - public function group($keys, $initial, $reduce, $options = []) - { - if (!($reduce instanceof \MongoCode)) { - $reduce = new \MongoCode((string)$reduce); - } - if (array_key_exists('condition', $options)) { - $options['condition'] = $this->buildCondition($options['condition']); - } - if (array_key_exists('finalize', $options)) { - if (!($options['finalize'] instanceof \MongoCode)) { - $options['finalize'] = new \MongoCode((string)$options['finalize']); - } - } - $token = $this->composeLogToken('group', [$keys, $initial, $reduce, $options]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - // Avoid possible E_DEPRECATED for $options: - if (empty($options)) { - $result = $this->mongoCollection->group($keys, $initial, $reduce); - } else { - $result = $this->mongoCollection->group($keys, $initial, $reduce, $options); - } - $this->tryResultError($result); - - Yii::endProfile($token, __METHOD__); - if (array_key_exists('retval', $result)) { - return $result['retval']; - } else { - return []; - } - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Performs aggregation using Mongo "map reduce" mechanism. - * Note: this function will not return the aggregation result, instead it will - * write it inside the another Mongo collection specified by "out" parameter. - * For example: - * - * ~~~ - * $customerCollection = Yii::$app->mongo->getCollection('customer'); - * $resultCollectionName = $customerCollection->mapReduce( - * 'function () {emit(this.status, this.amount)}', - * 'function (key, values) {return Array.sum(values)}', - * 'mapReduceOut', - * ['status' => 3] - * ); - * $query = new Query(); - * $results = $query->from($resultCollectionName)->all(); - * ~~~ - * - * @param \MongoCode|string $map function, which emits map data from collection. - * Argument will be automatically cast to [[\MongoCode]]. - * @param \MongoCode|string $reduce function that takes two arguments (the map key - * and the map values) and does the aggregation. - * Argument will be automatically cast to [[\MongoCode]]. - * @param string|array $out output collection name. It could be a string for simple output - * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']). - * You can pass ['inline' => true] to fetch the result at once without temporary collection usage. - * @param array $condition criteria for including a document in the aggregation. - * @param array $options additional optional parameters to the mapReduce command. Valid options include: - * - sort - array - key to sort the input documents. The sort key must be in an existing index for this collection. - * - limit - the maximum number of documents to return in the collection. - * - finalize - function, which follows the reduce method and modifies the output. - * - scope - array - specifies global variables that are accessible in the map, reduce and finalize functions. - * - jsMode - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions. - * - verbose - boolean - specifies whether to include the timing information in the result information. - * @return string|array the map reduce output collection name or output results. - * @throws Exception on failure. - */ - public function mapReduce($map, $reduce, $out, $condition = [], $options = []) - { - if (!($map instanceof \MongoCode)) { - $map = new \MongoCode((string)$map); - } - if (!($reduce instanceof \MongoCode)) { - $reduce = new \MongoCode((string)$reduce); - } - $command = [ - 'mapReduce' => $this->getName(), - 'map' => $map, - 'reduce' => $reduce, - 'out' => $out - ]; - if (!empty($condition)) { - $command['query'] = $this->buildCondition($condition); - } - if (array_key_exists('finalize', $options)) { - if (!($options['finalize'] instanceof \MongoCode)) { - $options['finalize'] = new \MongoCode((string)$options['finalize']); - } - } - if (!empty($options)) { - $command = array_merge($command, $options); - } - $token = $this->composeLogToken('mapReduce', [$map, $reduce, $out]); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $command = array_merge(['mapReduce' => $this->getName()], $command); - $result = $this->mongoCollection->db->command($command); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return array_key_exists('results', $result) ? $result['results'] : $result['result']; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Performs full text search. - * @param string $search string of terms that MongoDB parses and uses to query the text index. - * @param array $condition criteria for filtering a results list. - * @param array $fields list of fields to be returned in result. - * @param array $options additional optional parameters to the mapReduce command. Valid options include: - * - limit - the maximum number of documents to include in the response (by default 100). - * - language - the language that determines the list of stop words for the search - * and the rules for the stemmer and tokenizer. If not specified, the search uses the default - * language of the index. - * @return array the highest scoring documents, in descending order by score. - * @throws Exception on failure. - */ - public function fullTextSearch($search, $condition = [], $fields = [], $options = []) - { - $command = [ - 'search' => $search - ]; - if (!empty($condition)) { - $command['filter'] = $this->buildCondition($condition); - } - if (!empty($fields)) { - $command['project'] = $fields; - } - if (!empty($options)) { - $command = array_merge($command, $options); - } - $token = $this->composeLogToken('text', $command); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $command = array_merge(['text' => $this->getName()], $command); - $result = $this->mongoCollection->db->command($command); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return $result['results']; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Checks if command execution result ended with an error. - * @param mixed $result raw command execution result. - * @throws Exception if an error occurred. - */ - protected function tryResultError($result) - { - if (is_array($result)) { - if (!empty($result['errmsg'])) { - $errorMessage = $result['errmsg']; - } elseif (!empty($result['err'])) { - $errorMessage = $result['err']; - } - if (isset($errorMessage)) { - if (array_key_exists('code', $result)) { - $errorCode = (int)$result['code']; - } elseif (array_key_exists('ok', $result)) { - $errorCode = (int)$result['ok']; - } else { - $errorCode = 0; - } - throw new Exception($errorMessage, $errorCode); - } - } elseif (!$result) { - throw new Exception('Unknown error, use "w=1" option to enable error tracking'); - } - } - - /** - * Throws an exception if there was an error on the last operation. - * @throws Exception if an error occurred. - */ - protected function tryLastError() - { - $this->tryResultError($this->getLastError()); - } - - /** - * Converts "\yii\db\*" quick condition keyword into actual Mongo condition keyword. - * @param string $key raw condition key. - * @return string actual key. - */ - protected function normalizeConditionKeyword($key) - { - static $map = [ - 'OR' => '$or', - 'IN' => '$in', - 'NOT IN' => '$nin', - ]; - $matchKey = strtoupper($key); - if (array_key_exists($matchKey, $map)) { - return $map[$matchKey]; - } else { - return $key; - } - } - - /** - * Converts given value into [[MongoId]] instance. - * If array given, each element of it will be processed. - * @param mixed $rawId raw id(s). - * @return array|\MongoId normalized id(s). - */ - protected function ensureMongoId($rawId) - { - if (is_array($rawId)) { - $result = []; - foreach ($rawId as $key => $value) { - $result[$key] = $this->ensureMongoId($value); - } - return $result; - } elseif (is_object($rawId)) { - if ($rawId instanceof \MongoId) { - return $rawId; - } else { - $rawId = (string)$rawId; - } - } - try { - $mongoId = new \MongoId($rawId); - } catch (\MongoException $e) { - // invalid id format - $mongoId = $rawId; - } - return $mongoId; - } - - /** - * Parses the condition specification and generates the corresponding Mongo condition. - * @param array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @return array the generated Mongo condition - * @throws InvalidParamException if the condition is in bad format - */ - public function buildCondition($condition) - { - static $builders = [ - 'AND' => 'buildAndCondition', - 'OR' => 'buildOrCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - ]; - - if (!is_array($condition)) { - throw new InvalidParamException('Condition should be an array.'); - } elseif (empty($condition)) { - return []; - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition); - } else { - throw new InvalidParamException('Found unknown operator in query: ' . $operator); - } - } else { - // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition); - } - } - - /** - * Creates a condition based on column-value pairs. - * @param array $condition the condition specification. - * @return array the generated Mongo condition. - */ - public function buildHashCondition($condition) - { - $result = []; - foreach ($condition as $name => $value) { - if (strncmp('$', $name, 1) === 0) { - // Native Mongo condition: - $result[$name] = $value; - } else { - if (is_array($value)) { - if (array_key_exists(0, $value)) { - // Quick IN condition: - $result = array_merge($result, $this->buildInCondition('IN', [$name, $value])); - } else { - // Mongo complex condition: - $result[$name] = $value; - } - } else { - // Direct match: - if ($name == '_id') { - $value = $this->ensureMongoId($value); - } - $result[$name] = $value; - } - } - } - return $result; - } - - /** - * Connects two or more conditions with the `AND` operator. - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the Mongo conditions to connect. - * @return array the generated Mongo condition. - */ - public function buildAndCondition($operator, $operands) - { - $result = []; - foreach ($operands as $operand) { - $condition = $this->buildCondition($operand); - $result = array_merge_recursive($result, $condition); - } - return $result; - } - - /** - * Connects two or more conditions with the `OR` operator. - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the Mongo conditions to connect. - * @return array the generated Mongo condition. - */ - public function buildOrCondition($operator, $operands) - { - $operator = $this->normalizeConditionKeyword($operator); - $parts = []; - foreach ($operands as $operand) { - $parts[] = $this->buildCondition($operand); - } - return [$operator => $parts]; - } - - /** - * Creates an Mongo condition, which emulates the `BETWEEN` operator. - * @param string $operator the operator to use - * @param array $operands the first operand is the column name. The second and third operands - * describe the interval that column value should be in. - * @return array the generated Mongo condition. - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new InvalidParamException("Operator '$operator' requires three operands."); - } - list($column, $value1, $value2) = $operands; - if (strncmp('NOT', $operator, 3) === 0) { - return [ - $column => [ - '$lt' => $value1, - '$gt' => $value2, - ] - ]; - } else { - return [ - $column => [ - '$gte' => $value1, - '$lte' => $value2, - ] - ]; - } - } - - /** - * Creates an Mongo condition with the `IN` operator. - * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) - * @param array $operands the first operand is the column name. If it is an array - * a composite IN condition will be generated. - * The second operand is an array of values that column value should be among. - * @return array the generated Mongo condition. - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (!is_array($column)) { - $columns = [$column]; - $values = [$column => $values]; - } elseif (count($column) < 2) { - $columns = $column; - $values = [$column[0] => $values]; - } else { - $columns = $column; - } - - $operator = $this->normalizeConditionKeyword($operator); - $result = []; - foreach ($columns as $column) { - if ($column == '_id') { - $inValues = $this->ensureMongoId($values[$column]); - } else { - $inValues = $values[$column]; - } - $result[$column][$operator] = $inValues; - } - return $result; - } - - /** - * Creates a Mongo condition, which emulates the `LIKE` operator. - * @param string $operator the operator to use - * @param array $operands the first operand is the column name. - * The second operand is a single value that column value should be compared with. - * @return array the generated Mongo condition. - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildLikeCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - list($column, $value) = $operands; - if (!($value instanceof \MongoRegex)) { - $value = new \MongoRegex($value); - } - return [$column => $value]; - } + /** + * @var \MongoCollection Mongo collection instance. + */ + public $mongoCollection; + + /** + * @return string name of this collection. + */ + public function getName() + { + return $this->mongoCollection->getName(); + } + + /** + * @return string full name of this collection, including database name. + */ + public function getFullName() + { + return $this->mongoCollection->__toString(); + } + + /** + * @return array last error information. + */ + public function getLastError() + { + return $this->mongoCollection->db->lastError(); + } + + /** + * Composes log/profile token. + * @param string $command command name + * @param array $arguments command arguments. + * @return string token. + */ + protected function composeLogToken($command, $arguments = []) + { + $parts = []; + foreach ($arguments as $argument) { + $parts[] = is_scalar($argument) ? $argument : $this->encodeLogData($argument); + } + + return $this->getFullName() . '.' . $command . '(' . implode(', ', $parts) . ')'; + } + + /** + * Encodes complex log data into JSON format string. + * @param mixed $data raw data. + * @return string encoded data string. + */ + protected function encodeLogData($data) + { + return json_encode($this->processLogData($data)); + } + + /** + * Pre-processes the log data before sending it to `json_encode()`. + * @param mixed $data raw data. + * @return mixed the processed data. + */ + protected function processLogData($data) + { + if (is_object($data)) { + if ($data instanceof \MongoId || + $data instanceof \MongoRegex || + $data instanceof \MongoDate || + $data instanceof \MongoInt32 || + $data instanceof \MongoInt64 || + $data instanceof \MongoTimestamp + ) { + $data = get_class($data) . '(' . $data->__toString() . ')'; + } elseif ($data instanceof \MongoCode) { + $data = 'MongoCode( ' . $data->__toString() . ' )'; + } elseif ($data instanceof \MongoBinData) { + $data = 'MongoBinData(...)'; + } elseif ($data instanceof \MongoDBRef) { + $data = 'MongoDBRef(...)'; + } elseif ($data instanceof \MongoMinKey || $data instanceof \MongoMaxKey) { + $data = get_class($data); + } else { + $result = []; + foreach ($data as $name => $value) { + $result[$name] = $value; + } + $data = $result; + } + + if ($data === []) { + return new \stdClass(); + } + } + + if (is_array($data)) { + foreach ($data as $key => $value) { + if (is_array($value) || is_object($value)) { + $data[$key] = $this->processLogData($value); + } + } + } + + return $data; + } + + /** + * Drops this collection. + * @throws Exception on failure. + * @return boolean whether the operation successful. + */ + public function drop() + { + $token = $this->composeLogToken('drop'); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->drop(); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return true; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Creates an index on the collection and the specified fields. + * @param array|string $columns column name or list of column names. + * If array is given, each element in the array has as key the field name, and as + * value either 1 for ascending sort, or -1 for descending sort. + * You can specify field using native numeric key with the field name as a value, + * in this case ascending sort will be used. + * For example: + * ~~~ + * [ + * 'name', + * 'status' => -1, + * ] + * ~~~ + * @param array $options list of options in format: optionName => optionValue. + * @throws Exception on failure. + * @return boolean whether the operation successful. + */ + public function createIndex($columns, $options = []) + { + if (!is_array($columns)) { + $columns = [$columns]; + } + $keys = $this->normalizeIndexKeys($columns); + $token = $this->composeLogToken('createIndex', [$keys, $options]); + $options = array_merge(['w' => 1], $options); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->ensureIndex($keys, $options); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return true; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Drop indexes for specified column(s). + * @param string|array $columns column name or list of column names. + * If array is given, each element in the array has as key the field name, and as + * value either 1 for ascending sort, or -1 for descending sort. + * Use value 'text' to specify text index. + * You can specify field using native numeric key with the field name as a value, + * in this case ascending sort will be used. + * For example: + * ~~~ + * [ + * 'name', + * 'status' => -1, + * 'description' => 'text', + * ] + * ~~~ + * @throws Exception on failure. + * @return boolean whether the operation successful. + */ + public function dropIndex($columns) + { + if (!is_array($columns)) { + $columns = [$columns]; + } + $keys = $this->normalizeIndexKeys($columns); + $token = $this->composeLogToken('dropIndex', [$keys]); + Yii::info($token, __METHOD__); + try { + $result = $this->mongoCollection->deleteIndex($keys); + $this->tryResultError($result); + + return true; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Compose index keys from given columns/keys list. + * @param array $columns raw columns/keys list. + * @return array normalizes index keys array. + */ + protected function normalizeIndexKeys($columns) + { + $keys = []; + foreach ($columns as $key => $value) { + if (is_numeric($key)) { + $keys[$value] = \MongoCollection::ASCENDING; + } else { + $keys[$key] = $value; + } + } + + return $keys; + } + + /** + * Drops all indexes for this collection. + * @throws Exception on failure. + * @return integer count of dropped indexes. + */ + public function dropAllIndexes() + { + $token = $this->composeLogToken('dropIndexes'); + Yii::info($token, __METHOD__); + try { + $result = $this->mongoCollection->deleteIndexes(); + $this->tryResultError($result); + + return $result['nIndexesWas']; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Returns a cursor for the search results. + * In order to perform "find" queries use [[Query]] class. + * @param array $condition query condition + * @param array $fields fields to be selected + * @return \MongoCursor cursor for the search results + * @see Query + */ + public function find($condition = [], $fields = []) + { + return $this->mongoCollection->find($this->buildCondition($condition), $fields); + } + + /** + * Returns a single document. + * @param array $condition query condition + * @param array $fields fields to be selected + * @return array|null the single document. Null is returned if the query results in nothing. + * @see http://www.php.net/manual/en/mongocollection.findone.php + */ + public function findOne($condition = [], $fields = []) + { + return $this->mongoCollection->findOne($this->buildCondition($condition), $fields); + } + + /** + * Updates a document and returns it. + * @param array $condition query condition + * @param array $update update criteria + * @param array $fields fields to be returned + * @param array $options list of options in format: optionName => optionValue. + * @return array|null the original document, or the modified document when $options['new'] is set. + * @throws Exception on failure. + * @see http://www.php.net/manual/en/mongocollection.findandmodify.php + */ + public function findAndModify($condition, $update, $fields = [], $options = []) + { + $condition = $this->buildCondition($condition); + $token = $this->composeLogToken('findAndModify', [$condition, $update, $fields, $options]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->findAndModify($condition, $update, $fields, $options); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Inserts new data into collection. + * @param array|object $data data to be inserted. + * @param array $options list of options in format: optionName => optionValue. + * @return \MongoId new record id instance. + * @throws Exception on failure. + */ + public function insert($data, $options = []) + { + $token = $this->composeLogToken('insert', [$data]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $options = array_merge(['w' => 1], $options); + $this->tryResultError($this->mongoCollection->insert($data, $options)); + Yii::endProfile($token, __METHOD__); + + return is_array($data) ? $data['_id'] : $data->_id; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Inserts several new rows into collection. + * @param array $rows array of arrays or objects to be inserted. + * @param array $options list of options in format: optionName => optionValue. + * @return array inserted data, each row will have "_id" key assigned to it. + * @throws Exception on failure. + */ + public function batchInsert($rows, $options = []) + { + $token = $this->composeLogToken('batchInsert', [$rows]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $options = array_merge(['w' => 1], $options); + $this->tryResultError($this->mongoCollection->batchInsert($rows, $options)); + Yii::endProfile($token, __METHOD__); + + return $rows; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Updates the rows, which matches given criteria by given data. + * Note: for "multiple" mode Mongo requires explicit strategy "$set" or "$inc" + * to be specified for the "newData". If no strategy is passed "$set" will be used. + * @param array $condition description of the objects to update. + * @param array $newData the object with which to update the matching records. + * @param array $options list of options in format: optionName => optionValue. + * @return integer|boolean number of updated documents or whether operation was successful. + * @throws Exception on failure. + */ + public function update($condition, $newData, $options = []) + { + $condition = $this->buildCondition($condition); + $options = array_merge(['w' => 1, 'multiple' => true], $options); + if ($options['multiple']) { + $keys = array_keys($newData); + if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) { + $newData = ['$set' => $newData]; + } + } + $token = $this->composeLogToken('update', [$condition, $newData, $options]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->update($condition, $newData, $options); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + if (is_array($result) && array_key_exists('n', $result)) { + return $result['n']; + } else { + return true; + } + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Update the existing database data, otherwise insert this data + * @param array|object $data data to be updated/inserted. + * @param array $options list of options in format: optionName => optionValue. + * @return \MongoId updated/new record id instance. + * @throws Exception on failure. + */ + public function save($data, $options = []) + { + $token = $this->composeLogToken('save', [$data]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $options = array_merge(['w' => 1], $options); + $this->tryResultError($this->mongoCollection->save($data, $options)); + Yii::endProfile($token, __METHOD__); + + return is_array($data) ? $data['_id'] : $data->_id; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Removes data from the collection. + * @param array $condition description of records to remove. + * @param array $options list of options in format: optionName => optionValue. + * @return integer|boolean number of updated documents or whether operation was successful. + * @throws Exception on failure. + * @see http://www.php.net/manual/en/mongocollection.remove.php + */ + public function remove($condition = [], $options = []) + { + $condition = $this->buildCondition($condition); + $options = array_merge(['w' => 1, 'justOne' => false], $options); + $token = $this->composeLogToken('remove', [$condition, $options]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->remove($condition, $options); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + if (is_array($result) && array_key_exists('n', $result)) { + return $result['n']; + } else { + return true; + } + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Returns a list of distinct values for the given column across a collection. + * @param string $column column to use. + * @param array $condition query parameters. + * @return array|boolean array of distinct values, or "false" on failure. + * @throws Exception on failure. + */ + public function distinct($column, $condition = []) + { + $condition = $this->buildCondition($condition); + $token = $this->composeLogToken('distinct', [$column, $condition]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->distinct($column, $condition); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Performs aggregation using Mongo Aggregation Framework. + * @param array $pipeline list of pipeline operators, or just the first operator + * @param array $pipelineOperator additional pipeline operator. You can specify additional + * pipelines via third argument, fourth argument etc. + * @return array the result of the aggregation. + * @throws Exception on failure. + * @see http://docs.mongodb.org/manual/applications/aggregation/ + */ + public function aggregate($pipeline, $pipelineOperator = []) + { + $args = func_get_args(); + $token = $this->composeLogToken('aggregate', $args); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = call_user_func_array([$this->mongoCollection, 'aggregate'], $args); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return $result['result']; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Performs aggregation using Mongo "group" command. + * @param mixed $keys fields to group by. If an array or non-code object is passed, + * it will be the key used to group results. If instance of [[\MongoCode]] passed, + * it will be treated as a function that returns the key to group by. + * @param array $initial Initial value of the aggregation counter object. + * @param \MongoCode|string $reduce function that takes two arguments (the current + * document and the aggregation to this point) and does the aggregation. + * Argument will be automatically cast to [[\MongoCode]]. + * @param array $options optional parameters to the group command. Valid options include: + * - condition - criteria for including a document in the aggregation. + * - finalize - function called once per unique key that takes the final output of the reduce function. + * @return array the result of the aggregation. + * @throws Exception on failure. + * @see http://docs.mongodb.org/manual/reference/command/group/ + */ + public function group($keys, $initial, $reduce, $options = []) + { + if (!($reduce instanceof \MongoCode)) { + $reduce = new \MongoCode((string) $reduce); + } + if (array_key_exists('condition', $options)) { + $options['condition'] = $this->buildCondition($options['condition']); + } + if (array_key_exists('finalize', $options)) { + if (!($options['finalize'] instanceof \MongoCode)) { + $options['finalize'] = new \MongoCode((string) $options['finalize']); + } + } + $token = $this->composeLogToken('group', [$keys, $initial, $reduce, $options]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + // Avoid possible E_DEPRECATED for $options: + if (empty($options)) { + $result = $this->mongoCollection->group($keys, $initial, $reduce); + } else { + $result = $this->mongoCollection->group($keys, $initial, $reduce, $options); + } + $this->tryResultError($result); + + Yii::endProfile($token, __METHOD__); + if (array_key_exists('retval', $result)) { + return $result['retval']; + } else { + return []; + } + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Performs aggregation using Mongo "map reduce" mechanism. + * Note: this function will not return the aggregation result, instead it will + * write it inside the another Mongo collection specified by "out" parameter. + * For example: + * + * ~~~ + * $customerCollection = Yii::$app->mongo->getCollection('customer'); + * $resultCollectionName = $customerCollection->mapReduce( + * 'function () {emit(this.status, this.amount)}', + * 'function (key, values) {return Array.sum(values)}', + * 'mapReduceOut', + * ['status' => 3] + * ); + * $query = new Query(); + * $results = $query->from($resultCollectionName)->all(); + * ~~~ + * + * @param \MongoCode|string $map function, which emits map data from collection. + * Argument will be automatically cast to [[\MongoCode]]. + * @param \MongoCode|string $reduce function that takes two arguments (the map key + * and the map values) and does the aggregation. + * Argument will be automatically cast to [[\MongoCode]]. + * @param string|array $out output collection name. It could be a string for simple output + * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']). + * You can pass ['inline' => true] to fetch the result at once without temporary collection usage. + * @param array $condition criteria for including a document in the aggregation. + * @param array $options additional optional parameters to the mapReduce command. Valid options include: + * - sort - array - key to sort the input documents. The sort key must be in an existing index for this collection. + * - limit - the maximum number of documents to return in the collection. + * - finalize - function, which follows the reduce method and modifies the output. + * - scope - array - specifies global variables that are accessible in the map, reduce and finalize functions. + * - jsMode - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions. + * - verbose - boolean - specifies whether to include the timing information in the result information. + * @return string|array the map reduce output collection name or output results. + * @throws Exception on failure. + */ + public function mapReduce($map, $reduce, $out, $condition = [], $options = []) + { + if (!($map instanceof \MongoCode)) { + $map = new \MongoCode((string) $map); + } + if (!($reduce instanceof \MongoCode)) { + $reduce = new \MongoCode((string) $reduce); + } + $command = [ + 'mapReduce' => $this->getName(), + 'map' => $map, + 'reduce' => $reduce, + 'out' => $out + ]; + if (!empty($condition)) { + $command['query'] = $this->buildCondition($condition); + } + if (array_key_exists('finalize', $options)) { + if (!($options['finalize'] instanceof \MongoCode)) { + $options['finalize'] = new \MongoCode((string) $options['finalize']); + } + } + if (!empty($options)) { + $command = array_merge($command, $options); + } + $token = $this->composeLogToken('mapReduce', [$map, $reduce, $out]); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $command = array_merge(['mapReduce' => $this->getName()], $command); + $result = $this->mongoCollection->db->command($command); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return array_key_exists('results', $result) ? $result['results'] : $result['result']; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Performs full text search. + * @param string $search string of terms that MongoDB parses and uses to query the text index. + * @param array $condition criteria for filtering a results list. + * @param array $fields list of fields to be returned in result. + * @param array $options additional optional parameters to the mapReduce command. Valid options include: + * - limit - the maximum number of documents to include in the response (by default 100). + * - language - the language that determines the list of stop words for the search + * and the rules for the stemmer and tokenizer. If not specified, the search uses the default + * language of the index. + * @return array the highest scoring documents, in descending order by score. + * @throws Exception on failure. + */ + public function fullTextSearch($search, $condition = [], $fields = [], $options = []) + { + $command = [ + 'search' => $search + ]; + if (!empty($condition)) { + $command['filter'] = $this->buildCondition($condition); + } + if (!empty($fields)) { + $command['project'] = $fields; + } + if (!empty($options)) { + $command = array_merge($command, $options); + } + $token = $this->composeLogToken('text', $command); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $command = array_merge(['text' => $this->getName()], $command); + $result = $this->mongoCollection->db->command($command); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return $result['results']; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Checks if command execution result ended with an error. + * @param mixed $result raw command execution result. + * @throws Exception if an error occurred. + */ + protected function tryResultError($result) + { + if (is_array($result)) { + if (!empty($result['errmsg'])) { + $errorMessage = $result['errmsg']; + } elseif (!empty($result['err'])) { + $errorMessage = $result['err']; + } + if (isset($errorMessage)) { + if (array_key_exists('code', $result)) { + $errorCode = (int) $result['code']; + } elseif (array_key_exists('ok', $result)) { + $errorCode = (int) $result['ok']; + } else { + $errorCode = 0; + } + throw new Exception($errorMessage, $errorCode); + } + } elseif (!$result) { + throw new Exception('Unknown error, use "w=1" option to enable error tracking'); + } + } + + /** + * Throws an exception if there was an error on the last operation. + * @throws Exception if an error occurred. + */ + protected function tryLastError() + { + $this->tryResultError($this->getLastError()); + } + + /** + * Converts "\yii\db\*" quick condition keyword into actual Mongo condition keyword. + * @param string $key raw condition key. + * @return string actual key. + */ + protected function normalizeConditionKeyword($key) + { + static $map = [ + 'OR' => '$or', + 'IN' => '$in', + 'NOT IN' => '$nin', + ]; + $matchKey = strtoupper($key); + if (array_key_exists($matchKey, $map)) { + return $map[$matchKey]; + } else { + return $key; + } + } + + /** + * Converts given value into [[MongoId]] instance. + * If array given, each element of it will be processed. + * @param mixed $rawId raw id(s). + * @return array|\MongoId normalized id(s). + */ + protected function ensureMongoId($rawId) + { + if (is_array($rawId)) { + $result = []; + foreach ($rawId as $key => $value) { + $result[$key] = $this->ensureMongoId($value); + } + + return $result; + } elseif (is_object($rawId)) { + if ($rawId instanceof \MongoId) { + return $rawId; + } else { + $rawId = (string) $rawId; + } + } + try { + $mongoId = new \MongoId($rawId); + } catch (\MongoException $e) { + // invalid id format + $mongoId = $rawId; + } + + return $mongoId; + } + + /** + * Parses the condition specification and generates the corresponding Mongo condition. + * @param array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @return array the generated Mongo condition + * @throws InvalidParamException if the condition is in bad format + */ + public function buildCondition($condition) + { + static $builders = [ + 'AND' => 'buildAndCondition', + 'OR' => 'buildOrCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + ]; + + if (!is_array($condition)) { + throw new InvalidParamException('Condition should be an array.'); + } elseif (empty($condition)) { + return []; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + + return $this->$method($operator, $condition); + } else { + throw new InvalidParamException('Found unknown operator in query: ' . $operator); + } + } else { + // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + return $this->buildHashCondition($condition); + } + } + + /** + * Creates a condition based on column-value pairs. + * @param array $condition the condition specification. + * @return array the generated Mongo condition. + */ + public function buildHashCondition($condition) + { + $result = []; + foreach ($condition as $name => $value) { + if (strncmp('$', $name, 1) === 0) { + // Native Mongo condition: + $result[$name] = $value; + } else { + if (is_array($value)) { + if (array_key_exists(0, $value)) { + // Quick IN condition: + $result = array_merge($result, $this->buildInCondition('IN', [$name, $value])); + } else { + // Mongo complex condition: + $result[$name] = $value; + } + } else { + // Direct match: + if ($name == '_id') { + $value = $this->ensureMongoId($value); + } + $result[$name] = $value; + } + } + } + + return $result; + } + + /** + * Connects two or more conditions with the `AND` operator. + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the Mongo conditions to connect. + * @return array the generated Mongo condition. + */ + public function buildAndCondition($operator, $operands) + { + $result = []; + foreach ($operands as $operand) { + $condition = $this->buildCondition($operand); + $result = array_merge_recursive($result, $condition); + } + + return $result; + } + + /** + * Connects two or more conditions with the `OR` operator. + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the Mongo conditions to connect. + * @return array the generated Mongo condition. + */ + public function buildOrCondition($operator, $operands) + { + $operator = $this->normalizeConditionKeyword($operator); + $parts = []; + foreach ($operands as $operand) { + $parts[] = $this->buildCondition($operand); + } + + return [$operator => $parts]; + } + + /** + * Creates an Mongo condition, which emulates the `BETWEEN` operator. + * @param string $operator the operator to use + * @param array $operands the first operand is the column name. The second and third operands + * describe the interval that column value should be in. + * @return array the generated Mongo condition. + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildBetweenCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new InvalidParamException("Operator '$operator' requires three operands."); + } + list($column, $value1, $value2) = $operands; + if (strncmp('NOT', $operator, 3) === 0) { + return [ + $column => [ + '$lt' => $value1, + '$gt' => $value2, + ] + ]; + } else { + return [ + $column => [ + '$gte' => $value1, + '$lte' => $value2, + ] + ]; + } + } + + /** + * Creates an Mongo condition with the `IN` operator. + * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) + * @param array $operands the first operand is the column name. If it is an array + * a composite IN condition will be generated. + * The second operand is an array of values that column value should be among. + * @return array the generated Mongo condition. + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildInCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1])) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array) $values; + + if (!is_array($column)) { + $columns = [$column]; + $values = [$column => $values]; + } elseif (count($column) < 2) { + $columns = $column; + $values = [$column[0] => $values]; + } else { + $columns = $column; + } + + $operator = $this->normalizeConditionKeyword($operator); + $result = []; + foreach ($columns as $column) { + if ($column == '_id') { + $inValues = $this->ensureMongoId($values[$column]); + } else { + $inValues = $values[$column]; + } + $result[$column][$operator] = $inValues; + } + + return $result; + } + + /** + * Creates a Mongo condition, which emulates the `LIKE` operator. + * @param string $operator the operator to use + * @param array $operands the first operand is the column name. + * The second operand is a single value that column value should be compared with. + * @return array the generated Mongo condition. + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildLikeCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1])) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + list($column, $value) = $operands; + if (!($value instanceof \MongoRegex)) { + $value = new \MongoRegex($value); + } + + return [$column => $value]; + } } diff --git a/extensions/mongodb/Connection.php b/extensions/mongodb/Connection.php index 11a0c1a4db5..4d1aef9becc 100644 --- a/extensions/mongodb/Connection.php +++ b/extensions/mongodb/Connection.php @@ -72,201 +72,206 @@ */ class Connection extends Component { - /** - * @event Event an event that is triggered after a DB connection is established - */ - const EVENT_AFTER_OPEN = 'afterOpen'; + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; - /** - * @var string host:port - * - * Correct syntax is: - * mongodb://[username:password@]host1[:port1][,host2[:port2:],...][/dbname] - * For example: - * mongodb://localhost:27017 - * mongodb://developer:password@localhost:27017 - * mongodb://developer:password@localhost:27017/mydatabase - */ - public $dsn; - /** - * @var array connection options. - * for example: - * - * ~~~ - * [ - * 'socketTimeoutMS' => 1000, // how long a send or receive on a socket can take before timing out - * 'journal' => true // block write operations until the journal be flushed the to disk - * ] - * ~~~ - * - * @see http://www.php.net/manual/en/mongoclient.construct.php - */ - public $options = []; - /** - * @var string name of the Mongo database to use by default. - * If this field left blank, connection instance will attempt to determine it from - * [[options]] and [[dsn]] automatically, if needed. - */ - public $defaultDatabaseName; - /** - * @var \MongoClient Mongo client instance. - */ - public $mongoClient; - /** - * @var Database[] list of Mongo databases - */ - private $_databases = []; + /** + * @var string host:port + * + * Correct syntax is: + * mongodb://[username:password@]host1[:port1][,host2[:port2:],...][/dbname] + * For example: + * mongodb://localhost:27017 + * mongodb://developer:password@localhost:27017 + * mongodb://developer:password@localhost:27017/mydatabase + */ + public $dsn; + /** + * @var array connection options. + * for example: + * + * ~~~ + * [ + * 'socketTimeoutMS' => 1000, // how long a send or receive on a socket can take before timing out + * 'journal' => true // block write operations until the journal be flushed the to disk + * ] + * ~~~ + * + * @see http://www.php.net/manual/en/mongoclient.construct.php + */ + public $options = []; + /** + * @var string name of the Mongo database to use by default. + * If this field left blank, connection instance will attempt to determine it from + * [[options]] and [[dsn]] automatically, if needed. + */ + public $defaultDatabaseName; + /** + * @var \MongoClient Mongo client instance. + */ + public $mongoClient; + /** + * @var Database[] list of Mongo databases + */ + private $_databases = []; - /** - * Returns the Mongo collection with the given name. - * @param string|null $name collection name, if null default one will be used. - * @param boolean $refresh whether to reestablish the database connection even if it is found in the cache. - * @return Database database instance. - */ - public function getDatabase($name = null, $refresh = false) - { - if ($name === null) { - $name = $this->fetchDefaultDatabaseName(); - } - if ($refresh || !array_key_exists($name, $this->_databases)) { - $this->_databases[$name] = $this->selectDatabase($name); - } - return $this->_databases[$name]; - } + /** + * Returns the Mongo collection with the given name. + * @param string|null $name collection name, if null default one will be used. + * @param boolean $refresh whether to reestablish the database connection even if it is found in the cache. + * @return Database database instance. + */ + public function getDatabase($name = null, $refresh = false) + { + if ($name === null) { + $name = $this->fetchDefaultDatabaseName(); + } + if ($refresh || !array_key_exists($name, $this->_databases)) { + $this->_databases[$name] = $this->selectDatabase($name); + } - /** - * Returns [[defaultDatabaseName]] value, if it is not set, - * attempts to determine it from [[dsn]] value. - * @return string default database name - * @throws \yii\base\InvalidConfigException if unable to determine default database name. - */ - protected function fetchDefaultDatabaseName() - { - if ($this->defaultDatabaseName === null) { - if (isset($this->options['db'])) { - $this->defaultDatabaseName = $this->options['db']; - } elseif (preg_match('/^mongodb:\\/\\/.+\\/(.+)$/s', $this->dsn, $matches)) { - $this->defaultDatabaseName = $matches[1]; - } else { - throw new InvalidConfigException("Unable to determine default database name from dsn."); - } - } - return $this->defaultDatabaseName; - } + return $this->_databases[$name]; + } - /** - * Selects the database with given name. - * @param string $name database name. - * @return Database database instance. - */ - protected function selectDatabase($name) - { - $this->open(); - return Yii::createObject([ - 'class' => 'yii\mongodb\Database', - 'mongoDb' => $this->mongoClient->selectDB($name) - ]); - } + /** + * Returns [[defaultDatabaseName]] value, if it is not set, + * attempts to determine it from [[dsn]] value. + * @return string default database name + * @throws \yii\base\InvalidConfigException if unable to determine default database name. + */ + protected function fetchDefaultDatabaseName() + { + if ($this->defaultDatabaseName === null) { + if (isset($this->options['db'])) { + $this->defaultDatabaseName = $this->options['db']; + } elseif (preg_match('/^mongodb:\\/\\/.+\\/(.+)$/s', $this->dsn, $matches)) { + $this->defaultDatabaseName = $matches[1]; + } else { + throw new InvalidConfigException("Unable to determine default database name from dsn."); + } + } - /** - * Returns the Mongo collection with the given name. - * @param string|array $name collection name. If string considered as the name of the collection - * inside the default database. If array - first element considered as the name of the database, - * second - as name of collection inside that database - * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. - * @return Collection Mongo collection instance. - */ - public function getCollection($name, $refresh = false) - { - if (is_array($name)) { - list ($dbName, $collectionName) = $name; - return $this->getDatabase($dbName)->getCollection($collectionName, $refresh); - } else { - return $this->getDatabase()->getCollection($name, $refresh); - } - } + return $this->defaultDatabaseName; + } - /** - * Returns the Mongo GridFS collection. - * @param string|array $prefix collection prefix. If string considered as the prefix of the GridFS - * collection inside the default database. If array - first element considered as the name of the database, - * second - as prefix of the GridFS collection inside that database, if no second element present - * default "fs" prefix will be used. - * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. - * @return file\Collection Mongo GridFS collection instance. - */ - public function getFileCollection($prefix = 'fs', $refresh = false) - { - if (is_array($prefix)) { - list ($dbName, $collectionPrefix) = $prefix; - if (!isset($collectionPrefix)) { - $collectionPrefix = 'fs'; - } - return $this->getDatabase($dbName)->getFileCollection($collectionPrefix, $refresh); - } else { - return $this->getDatabase()->getFileCollection($prefix, $refresh); - } - } + /** + * Selects the database with given name. + * @param string $name database name. + * @return Database database instance. + */ + protected function selectDatabase($name) + { + $this->open(); - /** - * Returns a value indicating whether the Mongo connection is established. - * @return boolean whether the Mongo connection is established - */ - public function getIsActive() - { - return is_object($this->mongoClient) && $this->mongoClient->connected; - } + return Yii::createObject([ + 'class' => 'yii\mongodb\Database', + 'mongoDb' => $this->mongoClient->selectDB($name) + ]); + } - /** - * Establishes a Mongo connection. - * It does nothing if a Mongo connection has already been established. - * @throws Exception if connection fails - */ - public function open() - { - if ($this->mongoClient === null) { - if (empty($this->dsn)) { - throw new InvalidConfigException($this->className() . '::dsn cannot be empty.'); - } - $token = 'Opening MongoDB connection: ' . $this->dsn; - try { - Yii::trace($token, __METHOD__); - Yii::beginProfile($token, __METHOD__); - $options = $this->options; - $options['connect'] = true; - if ($this->defaultDatabaseName !== null) { - $options['db'] = $this->defaultDatabaseName; - } - $this->mongoClient = new \MongoClient($this->dsn, $options); - $this->initConnection(); - Yii::endProfile($token, __METHOD__); - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - } + /** + * Returns the Mongo collection with the given name. + * @param string|array $name collection name. If string considered as the name of the collection + * inside the default database. If array - first element considered as the name of the database, + * second - as name of collection inside that database + * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. + * @return Collection Mongo collection instance. + */ + public function getCollection($name, $refresh = false) + { + if (is_array($name)) { + list ($dbName, $collectionName) = $name; - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - if ($this->mongoClient !== null) { - Yii::trace('Closing MongoDB connection: ' . $this->dsn, __METHOD__); - $this->mongoClient = null; - $this->_databases = []; - } - } + return $this->getDatabase($dbName)->getCollection($collectionName, $refresh); + } else { + return $this->getDatabase()->getCollection($name, $refresh); + } + } - /** - * Initializes the DB connection. - * This method is invoked right after the DB connection is established. - * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. - */ - protected function initConnection() - { - $this->trigger(self::EVENT_AFTER_OPEN); - } + /** + * Returns the Mongo GridFS collection. + * @param string|array $prefix collection prefix. If string considered as the prefix of the GridFS + * collection inside the default database. If array - first element considered as the name of the database, + * second - as prefix of the GridFS collection inside that database, if no second element present + * default "fs" prefix will be used. + * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. + * @return file\Collection Mongo GridFS collection instance. + */ + public function getFileCollection($prefix = 'fs', $refresh = false) + { + if (is_array($prefix)) { + list ($dbName, $collectionPrefix) = $prefix; + if (!isset($collectionPrefix)) { + $collectionPrefix = 'fs'; + } + + return $this->getDatabase($dbName)->getFileCollection($collectionPrefix, $refresh); + } else { + return $this->getDatabase()->getFileCollection($prefix, $refresh); + } + } + + /** + * Returns a value indicating whether the Mongo connection is established. + * @return boolean whether the Mongo connection is established + */ + public function getIsActive() + { + return is_object($this->mongoClient) && $this->mongoClient->connected; + } + + /** + * Establishes a Mongo connection. + * It does nothing if a Mongo connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->mongoClient === null) { + if (empty($this->dsn)) { + throw new InvalidConfigException($this->className() . '::dsn cannot be empty.'); + } + $token = 'Opening MongoDB connection: ' . $this->dsn; + try { + Yii::trace($token, __METHOD__); + Yii::beginProfile($token, __METHOD__); + $options = $this->options; + $options['connect'] = true; + if ($this->defaultDatabaseName !== null) { + $options['db'] = $this->defaultDatabaseName; + } + $this->mongoClient = new \MongoClient($this->dsn, $options); + $this->initConnection(); + Yii::endProfile($token, __METHOD__); + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + if ($this->mongoClient !== null) { + Yii::trace('Closing MongoDB connection: ' . $this->dsn, __METHOD__); + $this->mongoClient = null; + $this->_databases = []; + } + } + + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } } diff --git a/extensions/mongodb/Database.php b/extensions/mongodb/Database.php index 2ded6b376a0..a560dbf57c0 100644 --- a/extensions/mongodb/Database.php +++ b/extensions/mongodb/Database.php @@ -22,152 +22,156 @@ */ class Database extends Object { - /** - * @var \MongoDB Mongo database instance. - */ - public $mongoDb; - /** - * @var Collection[] list of collections. - */ - private $_collections = []; - /** - * @var file\Collection[] list of GridFS collections. - */ - private $_fileCollections = []; + /** + * @var \MongoDB Mongo database instance. + */ + public $mongoDb; + /** + * @var Collection[] list of collections. + */ + private $_collections = []; + /** + * @var file\Collection[] list of GridFS collections. + */ + private $_fileCollections = []; - /** - * @return string name of this database. - */ - public function getName() - { - return $this->mongoDb->__toString(); - } + /** + * @return string name of this database. + */ + public function getName() + { + return $this->mongoDb->__toString(); + } - /** - * Returns the Mongo collection with the given name. - * @param string $name collection name - * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. - * @return Collection Mongo collection instance. - */ - public function getCollection($name, $refresh = false) - { - if ($refresh || !array_key_exists($name, $this->_collections)) { - $this->_collections[$name] = $this->selectCollection($name); - } - return $this->_collections[$name]; - } + /** + * Returns the Mongo collection with the given name. + * @param string $name collection name + * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. + * @return Collection Mongo collection instance. + */ + public function getCollection($name, $refresh = false) + { + if ($refresh || !array_key_exists($name, $this->_collections)) { + $this->_collections[$name] = $this->selectCollection($name); + } - /** - * Returns Mongo GridFS collection with given prefix. - * @param string $prefix collection prefix. - * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. - * @return file\Collection Mongo GridFS collection. - */ - public function getFileCollection($prefix = 'fs', $refresh = false) - { - if ($refresh || !array_key_exists($prefix, $this->_fileCollections)) { - $this->_fileCollections[$prefix] = $this->selectFileCollection($prefix); - } - return $this->_fileCollections[$prefix]; - } + return $this->_collections[$name]; + } - /** - * Selects collection with given name. - * @param string $name collection name. - * @return Collection collection instance. - */ - protected function selectCollection($name) - { - return Yii::createObject([ - 'class' => 'yii\mongodb\Collection', - 'mongoCollection' => $this->mongoDb->selectCollection($name) - ]); - } + /** + * Returns Mongo GridFS collection with given prefix. + * @param string $prefix collection prefix. + * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. + * @return file\Collection Mongo GridFS collection. + */ + public function getFileCollection($prefix = 'fs', $refresh = false) + { + if ($refresh || !array_key_exists($prefix, $this->_fileCollections)) { + $this->_fileCollections[$prefix] = $this->selectFileCollection($prefix); + } - /** - * Selects GridFS collection with given prefix. - * @param string $prefix file collection prefix. - * @return file\Collection file collection instance. - */ - protected function selectFileCollection($prefix) - { - return Yii::createObject([ - 'class' => 'yii\mongodb\file\Collection', - 'mongoCollection' => $this->mongoDb->getGridFS($prefix) - ]); - } + return $this->_fileCollections[$prefix]; + } - /** - * Creates new collection. - * Note: Mongo creates new collections automatically on the first demand, - * this method makes sense only for the migration script or for the case - * you need to create collection with the specific options. - * @param string $name name of the collection - * @param array $options collection options in format: "name" => "value" - * @return \MongoCollection new Mongo collection instance. - * @throws Exception on failure. - */ - public function createCollection($name, $options = []) - { - $token = $this->getName() . '.create(' . $name . ', ' . Json::encode($options) . ')'; - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoDb->createCollection($name, $options); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } + /** + * Selects collection with given name. + * @param string $name collection name. + * @return Collection collection instance. + */ + protected function selectCollection($name) + { + return Yii::createObject([ + 'class' => 'yii\mongodb\Collection', + 'mongoCollection' => $this->mongoDb->selectCollection($name) + ]); + } - /** - * Executes Mongo command. - * @param array $command command specification. - * @param array $options options in format: "name" => "value" - * @return array database response. - * @throws Exception on failure. - */ - public function executeCommand($command, $options = []) - { - $token = $this->getName() . '.$cmd(' . Json::encode($command) . ', ' . Json::encode($options) . ')'; - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoDb->command($command, $options); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } + /** + * Selects GridFS collection with given prefix. + * @param string $prefix file collection prefix. + * @return file\Collection file collection instance. + */ + protected function selectFileCollection($prefix) + { + return Yii::createObject([ + 'class' => 'yii\mongodb\file\Collection', + 'mongoCollection' => $this->mongoDb->getGridFS($prefix) + ]); + } - /** - * Checks if command execution result ended with an error. - * @param mixed $result raw command execution result. - * @throws Exception if an error occurred. - */ - protected function tryResultError($result) - { - if (is_array($result)) { - if (!empty($result['errmsg'])) { - $errorMessage = $result['errmsg']; - } elseif (!empty($result['err'])) { - $errorMessage = $result['err']; - } - if (isset($errorMessage)) { - if (array_key_exists('ok', $result)) { - $errorCode = (int)$result['ok']; - } else { - $errorCode = 0; - } - throw new Exception($errorMessage, $errorCode); - } - } elseif (!$result) { - throw new Exception('Unknown error, use "w=1" option to enable error tracking'); - } - } + /** + * Creates new collection. + * Note: Mongo creates new collections automatically on the first demand, + * this method makes sense only for the migration script or for the case + * you need to create collection with the specific options. + * @param string $name name of the collection + * @param array $options collection options in format: "name" => "value" + * @return \MongoCollection new Mongo collection instance. + * @throws Exception on failure. + */ + public function createCollection($name, $options = []) + { + $token = $this->getName() . '.create(' . $name . ', ' . Json::encode($options) . ')'; + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoDb->createCollection($name, $options); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Executes Mongo command. + * @param array $command command specification. + * @param array $options options in format: "name" => "value" + * @return array database response. + * @throws Exception on failure. + */ + public function executeCommand($command, $options = []) + { + $token = $this->getName() . '.$cmd(' . Json::encode($command) . ', ' . Json::encode($options) . ')'; + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoDb->command($command, $options); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Checks if command execution result ended with an error. + * @param mixed $result raw command execution result. + * @throws Exception if an error occurred. + */ + protected function tryResultError($result) + { + if (is_array($result)) { + if (!empty($result['errmsg'])) { + $errorMessage = $result['errmsg']; + } elseif (!empty($result['err'])) { + $errorMessage = $result['err']; + } + if (isset($errorMessage)) { + if (array_key_exists('ok', $result)) { + $errorCode = (int) $result['ok']; + } else { + $errorCode = 0; + } + throw new Exception($errorMessage, $errorCode); + } + } elseif (!$result) { + throw new Exception('Unknown error, use "w=1" option to enable error tracking'); + } + } } diff --git a/extensions/mongodb/Exception.php b/extensions/mongodb/Exception.php index a9385612b4e..3cc5ae48b2f 100644 --- a/extensions/mongodb/Exception.php +++ b/extensions/mongodb/Exception.php @@ -15,11 +15,11 @@ */ class Exception extends \yii\base\Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'MongoDB Exception'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'MongoDB Exception'; + } } diff --git a/extensions/mongodb/Query.php b/extensions/mongodb/Query.php index 6594a5702bc..3058ea084c8 100644 --- a/extensions/mongodb/Query.php +++ b/extensions/mongodb/Query.php @@ -38,309 +38,318 @@ */ class Query extends Component implements QueryInterface { - use QueryTrait; + use QueryTrait; - /** - * @var array the fields of the results to return. For example, `['name', 'group_id']`. - * The "_id" field is always returned. If not set, if means selecting all columns. - * @see select() - */ - public $select = []; - /** - * @var string|array the collection to be selected from. If string considered as the name of the collection - * inside the default database. If array - first element considered as the name of the database, - * second - as name of collection inside that database - * @see from() - */ - public $from; + /** + * @var array the fields of the results to return. For example, `['name', 'group_id']`. + * The "_id" field is always returned. If not set, if means selecting all columns. + * @see select() + */ + public $select = []; + /** + * @var string|array the collection to be selected from. If string considered as the name of the collection + * inside the default database. If array - first element considered as the name of the database, + * second - as name of collection inside that database + * @see from() + */ + public $from; - /** - * Returns the Mongo collection for this query. - * @param Connection $db Mongo connection. - * @return Collection collection instance. - */ - public function getCollection($db = null) - { - if ($db === null) { - $db = Yii::$app->getComponent('mongodb'); - } - return $db->getCollection($this->from); - } + /** + * Returns the Mongo collection for this query. + * @param Connection $db Mongo connection. + * @return Collection collection instance. + */ + public function getCollection($db = null) + { + if ($db === null) { + $db = Yii::$app->getComponent('mongodb'); + } - /** - * Sets the list of fields of the results to return. - * @param array $fields fields of the results to return. - * @return static the query object itself. - */ - public function select(array $fields) - { - $this->select = $fields; - return $this; - } + return $db->getCollection($this->from); + } - /** - * Sets the collection to be selected from. - * @param string|array the collection to be selected from. If string considered as the name of the collection - * inside the default database. If array - first element considered as the name of the database, - * second - as name of collection inside that database - * @return static the query object itself. - */ - public function from($collection) - { - $this->from = $collection; - return $this; - } + /** + * Sets the list of fields of the results to return. + * @param array $fields fields of the results to return. + * @return static the query object itself. + */ + public function select(array $fields) + { + $this->select = $fields; - /** - * Builds the Mongo cursor for this query. - * @param Connection $db the database connection used to execute the query. - * @return \MongoCursor mongo cursor instance. - */ - protected function buildCursor($db = null) - { - if ($this->where === null) { - $where = []; - } else { - $where = $this->where; - } - $selectFields = []; - if (!empty($this->select)) { - foreach ($this->select as $fieldName) { - $selectFields[$fieldName] = true; - } - } - $cursor = $this->getCollection($db)->find($where, $selectFields); - if (!empty($this->orderBy)) { - $sort = []; - foreach ($this->orderBy as $fieldName => $sortOrder) { - $sort[$fieldName] = $sortOrder === SORT_DESC ? \MongoCollection::DESCENDING : \MongoCollection::ASCENDING; - } - $cursor->sort($sort); - } - $cursor->limit($this->limit); - $cursor->skip($this->offset); - return $cursor; - } + return $this; + } - /** - * Fetches rows from the given Mongo cursor. - * @param \MongoCursor $cursor Mongo cursor instance to fetch data from. - * @param boolean $all whether to fetch all rows or only first one. - * @param string|callable $indexBy the column name or PHP callback, - * by which the query results should be indexed by. - * @throws Exception on failure. - * @return array|boolean result. - */ - protected function fetchRows($cursor, $all = true, $indexBy = null) - { - $token = 'find(' . Json::encode($cursor->info()) . ')'; - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->fetchRowsInternal($cursor, $all, $indexBy); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } + /** + * Sets the collection to be selected from. + * @param string|array the collection to be selected from. If string considered as the name of the collection + * inside the default database. If array - first element considered as the name of the database, + * second - as name of collection inside that database + * @return static the query object itself. + */ + public function from($collection) + { + $this->from = $collection; - /** - * @param \MongoCursor $cursor Mongo cursor instance to fetch data from. - * @param boolean $all whether to fetch all rows or only first one. - * @param string|callable $indexBy value to index by. - * @return array|boolean result. - * @see Query::fetchRows() - */ - protected function fetchRowsInternal($cursor, $all, $indexBy) - { - $result = []; - if ($all) { - foreach ($cursor as $row) { - if ($indexBy !== null) { - if (is_string($indexBy)) { - $key = $row[$indexBy]; - } else { - $key = call_user_func($indexBy, $row); - } - $result[$key] = $row; - } else { - $result[] = $row; - } - } - } else { - if ($cursor->hasNext()) { - $result = $cursor->getNext(); - } else { - $result = false; - } - } - return $result; - } + return $this; + } - /** - * Executes the query and returns all results as an array. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $cursor = $this->buildCursor($db); - return $this->fetchRows($cursor, true, $this->indexBy); - } + /** + * Builds the Mongo cursor for this query. + * @param Connection $db the database connection used to execute the query. + * @return \MongoCursor mongo cursor instance. + */ + protected function buildCursor($db = null) + { + if ($this->where === null) { + $where = []; + } else { + $where = $this->where; + } + $selectFields = []; + if (!empty($this->select)) { + foreach ($this->select as $fieldName) { + $selectFields[$fieldName] = true; + } + } + $cursor = $this->getCollection($db)->find($where, $selectFields); + if (!empty($this->orderBy)) { + $sort = []; + foreach ($this->orderBy as $fieldName => $sortOrder) { + $sort[$fieldName] = $sortOrder === SORT_DESC ? \MongoCollection::DESCENDING : \MongoCollection::ASCENDING; + } + $cursor->sort($sort); + } + $cursor->limit($this->limit); + $cursor->skip($this->offset); - /** - * Executes the query and returns a single row of result. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - public function one($db = null) - { - $cursor = $this->buildCursor($db); - return $this->fetchRows($cursor, false); - } + return $cursor; + } - /** - * Returns the number of records. - * @param string $q kept to match [[QueryInterface]], its value is ignored. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return integer number of records - * @throws Exception on failure. - */ - public function count($q = '*', $db = null) - { - $cursor = $this->buildCursor($db); - $token = 'find.count(' . Json::encode($cursor->info()) . ')'; - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $cursor->count(); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } + /** + * Fetches rows from the given Mongo cursor. + * @param \MongoCursor $cursor Mongo cursor instance to fetch data from. + * @param boolean $all whether to fetch all rows or only first one. + * @param string|callable $indexBy the column name or PHP callback, + * by which the query results should be indexed by. + * @throws Exception on failure. + * @return array|boolean result. + */ + protected function fetchRows($cursor, $all = true, $indexBy = null) + { + $token = 'find(' . Json::encode($cursor->info()) . ')'; + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->fetchRowsInternal($cursor, $all, $indexBy); + Yii::endProfile($token, __METHOD__); - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - return $this->one($db) !== null; - } + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } - /** - * Returns the sum of the specified column values. - * @param string $q the column name. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return integer the sum of the specified column values - */ - public function sum($q, $db = null) - { - return $this->aggregate($q, 'sum', $db); - } + /** + * @param \MongoCursor $cursor Mongo cursor instance to fetch data from. + * @param boolean $all whether to fetch all rows or only first one. + * @param string|callable $indexBy value to index by. + * @return array|boolean result. + * @see Query::fetchRows() + */ + protected function fetchRowsInternal($cursor, $all, $indexBy) + { + $result = []; + if ($all) { + foreach ($cursor as $row) { + if ($indexBy !== null) { + if (is_string($indexBy)) { + $key = $row[$indexBy]; + } else { + $key = call_user_func($indexBy, $row); + } + $result[$key] = $row; + } else { + $result[] = $row; + } + } + } else { + if ($cursor->hasNext()) { + $result = $cursor->getNext(); + } else { + $result = false; + } + } - /** - * Returns the average of the specified column values. - * @param string $q the column name. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return integer the average of the specified column values. - */ - public function average($q, $db = null) - { - return $this->aggregate($q, 'avg', $db); - } + return $result; + } - /** - * Returns the minimum of the specified column values. - * @param string $q the column name. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return integer the minimum of the specified column values. - */ - public function min($q, $db = null) - { - return $this->aggregate($q, 'min', $db); - } + /** + * Executes the query and returns all results as an array. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $cursor = $this->buildCursor($db); - /** - * Returns the maximum of the specified column values. - * @param string $q the column name. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return integer the maximum of the specified column values. - */ - public function max($q, $db = null) - { - return $this->aggregate($q, 'max', $db); - } + return $this->fetchRows($cursor, true, $this->indexBy); + } - /** - * Performs the aggregation for the given column. - * @param string $column column name. - * @param string $operator aggregation operator. - * @param Connection $db the database connection used to execute the query. - * @return integer aggregation result. - */ - protected function aggregate($column, $operator, $db) - { - $collection = $this->getCollection($db); - $pipelines = []; - if ($this->where !== null) { - $pipelines[] = ['$match' => $collection->buildCondition($this->where)]; - } - $pipelines[] = [ - '$group' => [ - '_id' => '1', - 'total' => [ - '$' . $operator => '$' . $column - ], - ] - ]; - $result = $collection->aggregate($pipelines); - if (array_key_exists(0, $result)) { - return $result[0]['total']; - } else { - return 0; - } - } + /** + * Executes the query and returns a single row of result. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null) + { + $cursor = $this->buildCursor($db); - /** - * Returns a list of distinct values for the given column across a collection. - * @param string $q column to use. - * @param Connection $db the Mongo connection used to execute the query. - * If this parameter is not given, the `mongodb` application component will be used. - * @return array array of distinct values - */ - public function distinct($q, $db = null) - { - $collection = $this->getCollection($db); - if ($this->where !== null) { - $condition = $this->where; - } else { - $condition = []; - } - $result = $collection->distinct($q, $condition); - if ($result === false) { - return []; - } else { - return $result; - } - } + return $this->fetchRows($cursor, false); + } + + /** + * Returns the number of records. + * @param string $q kept to match [[QueryInterface]], its value is ignored. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return integer number of records + * @throws Exception on failure. + */ + public function count($q = '*', $db = null) + { + $cursor = $this->buildCursor($db); + $token = 'find.count(' . Json::encode($cursor->info()) . ')'; + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $cursor->count(); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + return $this->one($db) !== null; + } + + /** + * Returns the sum of the specified column values. + * @param string $q the column name. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return integer the sum of the specified column values + */ + public function sum($q, $db = null) + { + return $this->aggregate($q, 'sum', $db); + } + + /** + * Returns the average of the specified column values. + * @param string $q the column name. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($q, $db = null) + { + return $this->aggregate($q, 'avg', $db); + } + + /** + * Returns the minimum of the specified column values. + * @param string $q the column name. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($q, $db = null) + { + return $this->aggregate($q, 'min', $db); + } + + /** + * Returns the maximum of the specified column values. + * @param string $q the column name. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($q, $db = null) + { + return $this->aggregate($q, 'max', $db); + } + + /** + * Performs the aggregation for the given column. + * @param string $column column name. + * @param string $operator aggregation operator. + * @param Connection $db the database connection used to execute the query. + * @return integer aggregation result. + */ + protected function aggregate($column, $operator, $db) + { + $collection = $this->getCollection($db); + $pipelines = []; + if ($this->where !== null) { + $pipelines[] = ['$match' => $collection->buildCondition($this->where)]; + } + $pipelines[] = [ + '$group' => [ + '_id' => '1', + 'total' => [ + '$' . $operator => '$' . $column + ], + ] + ]; + $result = $collection->aggregate($pipelines); + if (array_key_exists(0, $result)) { + return $result[0]['total']; + } else { + return 0; + } + } + + /** + * Returns a list of distinct values for the given column across a collection. + * @param string $q column to use. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongodb` application component will be used. + * @return array array of distinct values + */ + public function distinct($q, $db = null) + { + $collection = $this->getCollection($db); + if ($this->where !== null) { + $condition = $this->where; + } else { + $condition = []; + } + $result = $collection->distinct($q, $condition); + if ($result === false) { + return []; + } else { + return $result; + } + } } diff --git a/extensions/mongodb/Session.php b/extensions/mongodb/Session.php index efa78b6e494..a969f75eb69 100644 --- a/extensions/mongodb/Session.php +++ b/extensions/mongodb/Session.php @@ -35,156 +35,160 @@ */ class Session extends \yii\web\Session { - /** - * @var Connection|string the MongoDB connection object or the application component ID of the MongoDB connection. - * After the Session object is created, if you want to change this property, you should only assign it - * with a MongoDB connection object. - */ - public $db = 'mongodb'; - /** - * @var string|array the name of the MongoDB collection that stores the session data. - * Please refer to [[Connection::getCollection()]] on how to specify this parameter. - * This collection is better to be pre-created with fields 'id' and 'expire' indexed. - */ - public $sessionCollection = 'session'; - - /** - * Initializes the Session component. - * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException($this->className() . "::db must be either a MongoDB connection instance or the application component ID of a MongoDB connection."); - } - parent::init(); - } - - /** - * Returns a value indicating whether to use custom session storage. - * This method overrides the parent implementation and always returns true. - * @return boolean whether to use custom storage. - */ - public function getUseCustomStorage() - { - return true; - } - - /** - * Updates the current session ID with a newly generated one. - * Please refer to for more details. - * @param boolean $deleteOldSession Whether to delete the old associated session file or not. - */ - public function regenerateID($deleteOldSession = false) - { - $oldID = session_id(); - - // if no session is started, there is nothing to regenerate - if (empty($oldID)) { - return; - } - - parent::regenerateID(false); - $newID = session_id(); - - $collection = $this->db->getCollection($this->sessionCollection); - $row = $collection->findOne(['id' => $oldID]); - if ($row !== null) { - if ($deleteOldSession) { - $collection->update(['id' => $oldID], ['id' => $newID]); - } else { - unset($row['_id']); - $row['id'] = $newID; - $collection->insert($row); - } - } else { - // shouldn't reach here normally - $collection->insert([ - 'id' => $newID, - 'expire' => time() + $this->getTimeout() - ]); - } - } - - /** - * Session read handler. - * Do not call this method directly. - * @param string $id session ID - * @return string the session data - */ - public function readSession($id) - { - $collection = $this->db->getCollection($this->sessionCollection); - $doc = $collection->findOne( - [ - 'id' => $id, - 'expire' => ['$gt' => time()], - ], - ['data' => 1, '_id' => 0] - ); - return isset($doc['data']) ? $doc['data'] : ''; - } - - /** - * Session write handler. - * Do not call this method directly. - * @param string $id session ID - * @param string $data session data - * @return boolean whether session write is successful - */ - public function writeSession($id, $data) - { - // exception must be caught in session write handler - // http://us.php.net/manual/en/function.session-set-save-handler.php - try { - $this->db->getCollection($this->sessionCollection)->update( - ['id' => $id], - [ - 'id' => $id, - 'data' => $data, - 'expire' => time() + $this->getTimeout(), - ], - ['upsert' => true] - ); - } catch (\Exception $e) { - if (YII_DEBUG) { - echo $e->getMessage(); - } - // it is too late to log an error message here - return false; - } - return true; - } - - /** - * Session destroy handler. - * Do not call this method directly. - * @param string $id session ID - * @return boolean whether session is destroyed successfully - */ - public function destroySession($id) - { - $this->db->getCollection($this->sessionCollection)->remove( - ['id' => $id], - ['justOne' => true] - ); - return true; - } - - /** - * Session GC (garbage collection) handler. - * Do not call this method directly. - * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. - * @return boolean whether session is GCed successfully - */ - public function gcSession($maxLifetime) - { - $this->db->getCollection($this->sessionCollection) - ->remove(['expire' => ['$lt' => time()]]); - return true; - } + /** + * @var Connection|string the MongoDB connection object or the application component ID of the MongoDB connection. + * After the Session object is created, if you want to change this property, you should only assign it + * with a MongoDB connection object. + */ + public $db = 'mongodb'; + /** + * @var string|array the name of the MongoDB collection that stores the session data. + * Please refer to [[Connection::getCollection()]] on how to specify this parameter. + * This collection is better to be pre-created with fields 'id' and 'expire' indexed. + */ + public $sessionCollection = 'session'; + + /** + * Initializes the Session component. + * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException($this->className() . "::db must be either a MongoDB connection instance or the application component ID of a MongoDB connection."); + } + parent::init(); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Updates the current session ID with a newly generated one. + * Please refer to for more details. + * @param boolean $deleteOldSession Whether to delete the old associated session file or not. + */ + public function regenerateID($deleteOldSession = false) + { + $oldID = session_id(); + + // if no session is started, there is nothing to regenerate + if (empty($oldID)) { + return; + } + + parent::regenerateID(false); + $newID = session_id(); + + $collection = $this->db->getCollection($this->sessionCollection); + $row = $collection->findOne(['id' => $oldID]); + if ($row !== null) { + if ($deleteOldSession) { + $collection->update(['id' => $oldID], ['id' => $newID]); + } else { + unset($row['_id']); + $row['id'] = $newID; + $collection->insert($row); + } + } else { + // shouldn't reach here normally + $collection->insert([ + 'id' => $newID, + 'expire' => time() + $this->getTimeout() + ]); + } + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $collection = $this->db->getCollection($this->sessionCollection); + $doc = $collection->findOne( + [ + 'id' => $id, + 'expire' => ['$gt' => time()], + ], + ['data' => 1, '_id' => 0] + ); + + return isset($doc['data']) ? $doc['data'] : ''; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + // exception must be caught in session write handler + // http://us.php.net/manual/en/function.session-set-save-handler.php + try { + $this->db->getCollection($this->sessionCollection)->update( + ['id' => $id], + [ + 'id' => $id, + 'data' => $data, + 'expire' => time() + $this->getTimeout(), + ], + ['upsert' => true] + ); + } catch (\Exception $e) { + if (YII_DEBUG) { + echo $e->getMessage(); + } + // it is too late to log an error message here + return false; + } + + return true; + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + $this->db->getCollection($this->sessionCollection)->remove( + ['id' => $id], + ['justOne' => true] + ); + + return true; + } + + /** + * Session GC (garbage collection) handler. + * Do not call this method directly. + * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. + * @return boolean whether session is GCed successfully + */ + public function gcSession($maxLifetime) + { + $this->db->getCollection($this->sessionCollection) + ->remove(['expire' => ['$lt' => time()]]); + + return true; + } } diff --git a/extensions/mongodb/file/ActiveQuery.php b/extensions/mongodb/file/ActiveQuery.php index d19043f8bb2..f8d4a203f46 100644 --- a/extensions/mongodb/file/ActiveQuery.php +++ b/extensions/mongodb/file/ActiveQuery.php @@ -37,84 +37,87 @@ */ class ActiveQuery extends Query implements ActiveQueryInterface { - use ActiveQueryTrait; - use ActiveRelationTrait; + use ActiveQueryTrait; + use ActiveRelationTrait; - /** - * Executes query and returns all results as an array. - * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query. - * If null, the Mongo connection returned by [[modelClass]] will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $cursor = $this->buildCursor($db); - $rows = $this->fetchRows($cursor); - if (!empty($rows)) { - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - return $models; - } else { - return []; - } - } + /** + * Executes query and returns all results as an array. + * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query. + * If null, the Mongo connection returned by [[modelClass]] will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $cursor = $this->buildCursor($db); + $rows = $this->fetchRows($cursor); + if (!empty($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } - /** - * Executes query and returns a single row of result. - * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query. - * If null, the Mongo connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - $row = parent::one($db); - if ($row !== false) { - if ($this->asArray) { - $model = $row; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - if (!$this->asArray) { - $model->afterFind(); - } - return $model; - } else { - return null; - } - } + return $models; + } else { + return []; + } + } - /** - * Returns the Mongo collection for this query. - * @param \yii\mongodb\Connection $db Mongo connection. - * @return Collection collection instance. - */ - public function getCollection($db = null) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } - if ($this->from === null) { - $this->from = $modelClass::collectionName(); - } - return $db->getFileCollection($this->from); - } + /** + * Executes query and returns a single row of result. + * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query. + * If null, the Mongo connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + $row = parent::one($db); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + if (!$this->asArray) { + $model->afterFind(); + } + + return $model; + } else { + return null; + } + } + + /** + * Returns the Mongo collection for this query. + * @param \yii\mongodb\Connection $db Mongo connection. + * @return Collection collection instance. + */ + public function getCollection($db = null) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } + if ($this->from === null) { + $this->from = $modelClass::collectionName(); + } + + return $db->getFileCollection($this->from); + } } diff --git a/extensions/mongodb/file/ActiveRecord.php b/extensions/mongodb/file/ActiveRecord.php index 17372ec1f45..eec4b7bef93 100644 --- a/extensions/mongodb/file/ActiveRecord.php +++ b/extensions/mongodb/file/ActiveRecord.php @@ -44,306 +44,311 @@ */ abstract class ActiveRecord extends \yii\mongodb\ActiveRecord { - /** - * Creates an [[ActiveQuery]] instance. - * - * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also - * by [[hasOne()]] and [[hasMany()]] to create a relational query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ActiveQuery($config); - } + /** + * Creates an [[ActiveQuery]] instance. + * + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also + * by [[hasOne()]] and [[hasMany()]] to create a relational query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); - /** - * Return the Mongo GridFS collection instance for this AR class. - * @return Collection collection instance. - */ - public static function getCollection() - { - return static::getDb()->getFileCollection(static::collectionName()); - } + return new ActiveQuery($config); + } - /** - * Returns the list of all attribute names of the model. - * This method could be overridden by child classes to define available attributes. - * Note: all attributes defined in base Active Record class should be always present - * in returned array. - * For example: - * ~~~ - * public function attributes() - * { - * return array_merge( - * parent::attributes(), - * ['tags', 'status'] - * ); - * } - * ~~~ - * @return array list of attribute names. - */ - public function attributes() - { - return [ - '_id', - 'filename', - 'uploadDate', - 'length', - 'chunkSize', - 'md5', - 'file', - 'newFileContent' - ]; - } + /** + * Return the Mongo GridFS collection instance for this AR class. + * @return Collection collection instance. + */ + public static function getCollection() + { + return static::getDb()->getFileCollection(static::collectionName()); + } - /** - * @see ActiveRecord::insert() - */ - protected function insertInternal($attributes = null) - { - if (!$this->beforeSave(true)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $currentAttributes = $this->getAttributes(); - foreach ($this->primaryKey() as $key) { - $values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null; - } - } - $collection = static::getCollection(); - if (isset($values['newFileContent'])) { - $newFileContent = $values['newFileContent']; - unset($values['newFileContent']); - } - if (isset($values['file'])) { - $newFile = $values['file']; - unset($values['file']); - } - if (isset($newFileContent)) { - $newId = $collection->insertFileContent($newFileContent, $values); - } elseif (isset($newFile)) { - $fileName = $this->extractFileName($newFile); - $newId = $collection->insertFile($fileName, $values); - } else { - $newId = $collection->insert($values); - } - $this->setAttribute('_id', $newId); - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $value); - } - $this->afterSave(true); - return true; - } + /** + * Returns the list of all attribute names of the model. + * This method could be overridden by child classes to define available attributes. + * Note: all attributes defined in base Active Record class should be always present + * in returned array. + * For example: + * ~~~ + * public function attributes() + * { + * return array_merge( + * parent::attributes(), + * ['tags', 'status'] + * ); + * } + * ~~~ + * @return array list of attribute names. + */ + public function attributes() + { + return [ + '_id', + 'filename', + 'uploadDate', + 'length', + 'chunkSize', + 'md5', + 'file', + 'newFileContent' + ]; + } - /** - * @see ActiveRecord::update() - * @throws StaleObjectException - */ - protected function updateInternal($attributes = null) - { - if (!$this->beforeSave(false)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $this->afterSave(false); - return 0; - } + /** + * @see ActiveRecord::insert() + */ + protected function insertInternal($attributes = null) + { + if (!$this->beforeSave(true)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $currentAttributes = $this->getAttributes(); + foreach ($this->primaryKey() as $key) { + $values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null; + } + } + $collection = static::getCollection(); + if (isset($values['newFileContent'])) { + $newFileContent = $values['newFileContent']; + unset($values['newFileContent']); + } + if (isset($values['file'])) { + $newFile = $values['file']; + unset($values['file']); + } + if (isset($newFileContent)) { + $newId = $collection->insertFileContent($newFileContent, $values); + } elseif (isset($newFile)) { + $fileName = $this->extractFileName($newFile); + $newId = $collection->insertFile($fileName, $values); + } else { + $newId = $collection->insert($values); + } + $this->setAttribute('_id', $newId); + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $value); + } + $this->afterSave(true); - $collection = static::getCollection(); - if (isset($values['newFileContent'])) { - $newFileContent = $values['newFileContent']; - unset($values['newFileContent']); - } - if (isset($values['file'])) { - $newFile = $values['file']; - unset($values['file']); - } - if (isset($newFileContent) || isset($newFile)) { - $fileAssociatedAttributeNames = [ - 'filename', - 'uploadDate', - 'length', - 'chunkSize', - 'md5', - 'file', - 'newFileContent' - ]; - $values = array_merge($this->getAttributes(null, $fileAssociatedAttributeNames), $values); - $rows = $this->deleteInternal(); - $insertValues = $values; - $insertValues['_id'] = $this->getAttribute('_id'); - if (isset($newFileContent)) { - $collection->insertFileContent($newFileContent, $insertValues); - } else { - $fileName = $this->extractFileName($newFile); - $collection->insertFile($fileName, $insertValues); - } - $this->setAttribute('newFileContent', null); - $this->setAttribute('file', null); - } else { - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - if (!isset($values[$lock])) { - $values[$lock] = $this->$lock + 1; - } - $condition[$lock] = $this->$lock; - } - // We do not check the return value of update() because it's possible - // that it doesn't change anything and thus returns 0. - $rows = $collection->update($condition, $values); - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); - } - } + return true; + } - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $this->getAttribute($name)); - } - $this->afterSave(false); - return $rows; - } + /** + * @see ActiveRecord::update() + * @throws StaleObjectException + */ + protected function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false); - /** - * Extracts filename from given raw file value. - * @param mixed $file raw file value. - * @return string file name. - * @throws \yii\base\InvalidParamException on invalid file value. - */ - protected function extractFileName($file) - { - if ($file instanceof UploadedFile) { - return $file->tempName; - } elseif (is_string($file)) { - if (file_exists($file)) { - return $file; - } else { - throw new InvalidParamException("File '{$file}' does not exist."); - } - } else { - throw new InvalidParamException('Unsupported type of "file" attribute.'); - } - } + return 0; + } - /** - * Refreshes the [[file]] attribute from file collection, using current primary key. - * @return \MongoGridFSFile|null refreshed file value. - */ - public function refreshFile() - { - $mongoFile = $this->getCollection()->get($this->getPrimaryKey()); - $this->setAttribute('file', $mongoFile); - return $mongoFile; - } + $collection = static::getCollection(); + if (isset($values['newFileContent'])) { + $newFileContent = $values['newFileContent']; + unset($values['newFileContent']); + } + if (isset($values['file'])) { + $newFile = $values['file']; + unset($values['file']); + } + if (isset($newFileContent) || isset($newFile)) { + $fileAssociatedAttributeNames = [ + 'filename', + 'uploadDate', + 'length', + 'chunkSize', + 'md5', + 'file', + 'newFileContent' + ]; + $values = array_merge($this->getAttributes(null, $fileAssociatedAttributeNames), $values); + $rows = $this->deleteInternal(); + $insertValues = $values; + $insertValues['_id'] = $this->getAttribute('_id'); + if (isset($newFileContent)) { + $collection->insertFileContent($newFileContent, $insertValues); + } else { + $fileName = $this->extractFileName($newFile); + $collection->insertFile($fileName, $insertValues); + } + $this->setAttribute('newFileContent', null); + $this->setAttribute('file', null); + } else { + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } + // We do not check the return value of update() because it's possible + // that it doesn't change anything and thus returns 0. + $rows = $collection->update($condition, $values); + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + } - /** - * Returns the associated file content. - * @return null|string file content. - * @throws \yii\base\InvalidParamException on invalid file attribute value. - */ - public function getFileContent() - { - $file = $this->getAttribute('file'); - if (empty($file) && !$this->getIsNewRecord()) { - $file = $this->refreshFile(); - } - if (empty($file)) { - return null; - } elseif ($file instanceof \MongoGridFSFile) { - $fileSize = $file->getSize(); - if (empty($fileSize)) { - return null; - } else { - return $file->getBytes(); - } - } elseif ($file instanceof UploadedFile) { - return file_get_contents($file->tempName); - } elseif (is_string($file)) { - if (file_exists($file)) { - return file_get_contents($file); - } else { - throw new InvalidParamException("File '{$file}' does not exist."); - } - } else { - throw new InvalidParamException('Unsupported type of "file" attribute.'); - } - } + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $this->getAttribute($name)); + } + $this->afterSave(false); - /** - * Writes the the internal file content into the given filename. - * @param string $filename full filename to be written. - * @return boolean whether the operation was successful. - * @throws \yii\base\InvalidParamException on invalid file attribute value. - */ - public function writeFile($filename) - { - $file = $this->getAttribute('file'); - if (empty($file) && !$this->getIsNewRecord()) { - $file = $this->refreshFile(); - } - if (empty($file)) { - throw new InvalidParamException('There is no file associated with this object.'); - } elseif ($file instanceof \MongoGridFSFile) { - return ($file->write($filename) == $file->getSize()); - } elseif ($file instanceof UploadedFile) { - return copy($file->tempName, $filename); - } elseif (is_string($file)) { - if (file_exists($file)) { - return copy($file, $filename); - } else { - throw new InvalidParamException("File '{$file}' does not exist."); - } - } else { - throw new InvalidParamException('Unsupported type of "file" attribute.'); - } - } + return $rows; + } - /** - * This method returns a stream resource that can be used with all file functions in PHP, - * which deal with reading files. The contents of the file are pulled out of MongoDB on the fly, - * so that the whole file does not have to be loaded into memory first. - * @return resource file stream resource. - * @throws \yii\base\InvalidParamException on invalid file attribute value. - */ - public function getFileResource() - { - $file = $this->getAttribute('file'); - if (empty($file) && !$this->getIsNewRecord()) { - $file = $this->refreshFile(); - } - if (empty($file)) { - throw new InvalidParamException('There is no file associated with this object.'); - } elseif ($file instanceof \MongoGridFSFile) { - return $file->getResource(); - } elseif ($file instanceof UploadedFile) { - return fopen($file->tempName, 'r'); - } elseif (is_string($file)) { - if (file_exists($file)) { - return fopen($file, 'r'); - } else { - throw new InvalidParamException("File '{$file}' does not exist."); - } - } else { - throw new InvalidParamException('Unsupported type of "file" attribute.'); - } - } + /** + * Extracts filename from given raw file value. + * @param mixed $file raw file value. + * @return string file name. + * @throws \yii\base\InvalidParamException on invalid file value. + */ + protected function extractFileName($file) + { + if ($file instanceof UploadedFile) { + return $file->tempName; + } elseif (is_string($file)) { + if (file_exists($file)) { + return $file; + } else { + throw new InvalidParamException("File '{$file}' does not exist."); + } + } else { + throw new InvalidParamException('Unsupported type of "file" attribute.'); + } + } + + /** + * Refreshes the [[file]] attribute from file collection, using current primary key. + * @return \MongoGridFSFile|null refreshed file value. + */ + public function refreshFile() + { + $mongoFile = $this->getCollection()->get($this->getPrimaryKey()); + $this->setAttribute('file', $mongoFile); + + return $mongoFile; + } + + /** + * Returns the associated file content. + * @return null|string file content. + * @throws \yii\base\InvalidParamException on invalid file attribute value. + */ + public function getFileContent() + { + $file = $this->getAttribute('file'); + if (empty($file) && !$this->getIsNewRecord()) { + $file = $this->refreshFile(); + } + if (empty($file)) { + return null; + } elseif ($file instanceof \MongoGridFSFile) { + $fileSize = $file->getSize(); + if (empty($fileSize)) { + return null; + } else { + return $file->getBytes(); + } + } elseif ($file instanceof UploadedFile) { + return file_get_contents($file->tempName); + } elseif (is_string($file)) { + if (file_exists($file)) { + return file_get_contents($file); + } else { + throw new InvalidParamException("File '{$file}' does not exist."); + } + } else { + throw new InvalidParamException('Unsupported type of "file" attribute.'); + } + } + + /** + * Writes the the internal file content into the given filename. + * @param string $filename full filename to be written. + * @return boolean whether the operation was successful. + * @throws \yii\base\InvalidParamException on invalid file attribute value. + */ + public function writeFile($filename) + { + $file = $this->getAttribute('file'); + if (empty($file) && !$this->getIsNewRecord()) { + $file = $this->refreshFile(); + } + if (empty($file)) { + throw new InvalidParamException('There is no file associated with this object.'); + } elseif ($file instanceof \MongoGridFSFile) { + return ($file->write($filename) == $file->getSize()); + } elseif ($file instanceof UploadedFile) { + return copy($file->tempName, $filename); + } elseif (is_string($file)) { + if (file_exists($file)) { + return copy($file, $filename); + } else { + throw new InvalidParamException("File '{$file}' does not exist."); + } + } else { + throw new InvalidParamException('Unsupported type of "file" attribute.'); + } + } + + /** + * This method returns a stream resource that can be used with all file functions in PHP, + * which deal with reading files. The contents of the file are pulled out of MongoDB on the fly, + * so that the whole file does not have to be loaded into memory first. + * @return resource file stream resource. + * @throws \yii\base\InvalidParamException on invalid file attribute value. + */ + public function getFileResource() + { + $file = $this->getAttribute('file'); + if (empty($file) && !$this->getIsNewRecord()) { + $file = $this->refreshFile(); + } + if (empty($file)) { + throw new InvalidParamException('There is no file associated with this object.'); + } elseif ($file instanceof \MongoGridFSFile) { + return $file->getResource(); + } elseif ($file instanceof UploadedFile) { + return fopen($file->tempName, 'r'); + } elseif (is_string($file)) { + if (file_exists($file)) { + return fopen($file, 'r'); + } else { + throw new InvalidParamException("File '{$file}' does not exist."); + } + } else { + throw new InvalidParamException('Unsupported type of "file" attribute.'); + } + } } diff --git a/extensions/mongodb/file/Collection.php b/extensions/mongodb/file/Collection.php index c4083648ebb..4ecc4c5d445 100644 --- a/extensions/mongodb/file/Collection.php +++ b/extensions/mongodb/file/Collection.php @@ -24,162 +24,169 @@ */ class Collection extends \yii\mongodb\Collection { - /** - * @var \MongoGridFS Mongo GridFS collection instance. - */ - public $mongoCollection; - /** - * @var \yii\mongodb\Collection file chunks Mongo collection. - */ - private $_chunkCollection; - - /** - * Returns the Mongo collection for the file chunks. - * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. - * @return \yii\mongodb\Collection mongo collection instance. - */ - public function getChunkCollection($refresh = false) - { - if ($refresh || !is_object($this->_chunkCollection)) { - $this->_chunkCollection = Yii::createObject([ - 'class' => 'yii\mongodb\Collection', - 'mongoCollection' => $this->mongoCollection->chunks - ]); - } - return $this->_chunkCollection; - } - - /** - * Removes data from the collection. - * @param array $condition description of records to remove. - * @param array $options list of options in format: optionName => optionValue. - * @return integer|boolean number of updated documents or whether operation was successful. - * @throws Exception on failure. - */ - public function remove($condition = [], $options = []) - { - $result = parent::remove($condition, $options); - $this->tryLastError(); // MongoGridFS::remove will return even if the remove failed - return $result; - } - - /** - * Creates new file in GridFS collection from given local filesystem file. - * Additional attributes can be added file document using $metadata. - * @param string $filename name of the file to store. - * @param array $metadata other metadata fields to include in the file document. - * @param array $options list of options in format: optionName => optionValue - * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]] - * unless an "_id" was explicitly specified in the metadata. - * @throws Exception on failure. - */ - public function insertFile($filename, $metadata = [], $options = []) - { - $token = 'Inserting file into ' . $this->getFullName(); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $options = array_merge(['w' => 1], $options); - $result = $this->mongoCollection->storeFile($filename, $metadata, $options); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Creates new file in GridFS collection with specified content. - * Additional attributes can be added file document using $metadata. - * @param string $bytes string of bytes to store. - * @param array $metadata other metadata fields to include in the file document. - * @param array $options list of options in format: optionName => optionValue - * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]] - * unless an "_id" was explicitly specified in the metadata. - * @throws Exception on failure. - */ - public function insertFileContent($bytes, $metadata = [], $options = []) - { - $token = 'Inserting file content into ' . $this->getFullName(); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $options = array_merge(['w' => 1], $options); - $result = $this->mongoCollection->storeBytes($bytes, $metadata, $options); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Creates new file in GridFS collection from uploaded file. - * Additional attributes can be added file document using $metadata. - * @param string $name name of the uploaded file to store. This should correspond to - * the file field's name attribute in the HTML form. - * @param array $metadata other metadata fields to include in the file document. - * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]] - * unless an "_id" was explicitly specified in the metadata. - * @throws Exception on failure. - */ - public function insertUploads($name, $metadata = []) - { - $token = 'Inserting file uploads into ' . $this->getFullName(); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->storeUpload($name, $metadata); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Retrieves the file with given _id. - * @param mixed $id _id of the file to find. - * @return \MongoGridFSFile|null found file, or null if file does not exist - * @throws Exception on failure. - */ - public function get($id) - { - $token = 'Inserting file uploads into ' . $this->getFullName(); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->get($id); - Yii::endProfile($token, __METHOD__); - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } - - /** - * Deletes the file with given _id. - * @param mixed $id _id of the file to find. - * @return boolean whether the operation was successful. - * @throws Exception on failure. - */ - public function delete($id) - { - $token = 'Inserting file uploads into ' . $this->getFullName(); - Yii::info($token, __METHOD__); - try { - Yii::beginProfile($token, __METHOD__); - $result = $this->mongoCollection->delete($id); - $this->tryResultError($result); - Yii::endProfile($token, __METHOD__); - return true; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), (int)$e->getCode(), $e); - } - } + /** + * @var \MongoGridFS Mongo GridFS collection instance. + */ + public $mongoCollection; + /** + * @var \yii\mongodb\Collection file chunks Mongo collection. + */ + private $_chunkCollection; + + /** + * Returns the Mongo collection for the file chunks. + * @param boolean $refresh whether to reload the collection instance even if it is found in the cache. + * @return \yii\mongodb\Collection mongo collection instance. + */ + public function getChunkCollection($refresh = false) + { + if ($refresh || !is_object($this->_chunkCollection)) { + $this->_chunkCollection = Yii::createObject([ + 'class' => 'yii\mongodb\Collection', + 'mongoCollection' => $this->mongoCollection->chunks + ]); + } + + return $this->_chunkCollection; + } + + /** + * Removes data from the collection. + * @param array $condition description of records to remove. + * @param array $options list of options in format: optionName => optionValue. + * @return integer|boolean number of updated documents or whether operation was successful. + * @throws Exception on failure. + */ + public function remove($condition = [], $options = []) + { + $result = parent::remove($condition, $options); + $this->tryLastError(); // MongoGridFS::remove will return even if the remove failed + + return $result; + } + + /** + * Creates new file in GridFS collection from given local filesystem file. + * Additional attributes can be added file document using $metadata. + * @param string $filename name of the file to store. + * @param array $metadata other metadata fields to include in the file document. + * @param array $options list of options in format: optionName => optionValue + * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]] + * unless an "_id" was explicitly specified in the metadata. + * @throws Exception on failure. + */ + public function insertFile($filename, $metadata = [], $options = []) + { + $token = 'Inserting file into ' . $this->getFullName(); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $options = array_merge(['w' => 1], $options); + $result = $this->mongoCollection->storeFile($filename, $metadata, $options); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Creates new file in GridFS collection with specified content. + * Additional attributes can be added file document using $metadata. + * @param string $bytes string of bytes to store. + * @param array $metadata other metadata fields to include in the file document. + * @param array $options list of options in format: optionName => optionValue + * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]] + * unless an "_id" was explicitly specified in the metadata. + * @throws Exception on failure. + */ + public function insertFileContent($bytes, $metadata = [], $options = []) + { + $token = 'Inserting file content into ' . $this->getFullName(); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $options = array_merge(['w' => 1], $options); + $result = $this->mongoCollection->storeBytes($bytes, $metadata, $options); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Creates new file in GridFS collection from uploaded file. + * Additional attributes can be added file document using $metadata. + * @param string $name name of the uploaded file to store. This should correspond to + * the file field's name attribute in the HTML form. + * @param array $metadata other metadata fields to include in the file document. + * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]] + * unless an "_id" was explicitly specified in the metadata. + * @throws Exception on failure. + */ + public function insertUploads($name, $metadata = []) + { + $token = 'Inserting file uploads into ' . $this->getFullName(); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->storeUpload($name, $metadata); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Retrieves the file with given _id. + * @param mixed $id _id of the file to find. + * @return \MongoGridFSFile|null found file, or null if file does not exist + * @throws Exception on failure. + */ + public function get($id) + { + $token = 'Inserting file uploads into ' . $this->getFullName(); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->get($id); + Yii::endProfile($token, __METHOD__); + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } + + /** + * Deletes the file with given _id. + * @param mixed $id _id of the file to find. + * @return boolean whether the operation was successful. + * @throws Exception on failure. + */ + public function delete($id) + { + $token = 'Inserting file uploads into ' . $this->getFullName(); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $result = $this->mongoCollection->delete($id); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + + return true; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); + } + } } diff --git a/extensions/mongodb/file/Query.php b/extensions/mongodb/file/Query.php index 341113e2b2c..c801828fa11 100644 --- a/extensions/mongodb/file/Query.php +++ b/extensions/mongodb/file/Query.php @@ -23,53 +23,55 @@ */ class Query extends \yii\mongodb\Query { - /** - * Returns the Mongo collection for this query. - * @param \yii\mongodb\Connection $db Mongo connection. - * @return Collection collection instance. - */ - public function getCollection($db = null) - { - if ($db === null) { - $db = Yii::$app->getComponent('mongodb'); - } - return $db->getFileCollection($this->from); - } + /** + * Returns the Mongo collection for this query. + * @param \yii\mongodb\Connection $db Mongo connection. + * @return Collection collection instance. + */ + public function getCollection($db = null) + { + if ($db === null) { + $db = Yii::$app->getComponent('mongodb'); + } - /** - * @param \MongoGridFSCursor $cursor Mongo cursor instance to fetch data from. - * @param boolean $all whether to fetch all rows or only first one. - * @param string|callable $indexBy value to index by. - * @return array|boolean result. - * @see Query::fetchRows() - */ - protected function fetchRowsInternal($cursor, $all, $indexBy) - { - $result = []; - if ($all) { - foreach ($cursor as $file) { - $row = $file->file; - $row['file'] = $file; - if ($indexBy !== null) { - if (is_string($indexBy)) { - $key = $row[$indexBy]; - } else { - $key = call_user_func($indexBy, $row); - } - $result[$key] = $row; - } else { - $result[] = $row; - } - } - } else { - if ($cursor->hasNext()) { - $file = $cursor->getNext(); - $result = $file->file; - $result['file'] = $file; - } else { - $result = false; - } - } - return $result; - } + return $db->getFileCollection($this->from); + } + + /** + * @param \MongoGridFSCursor $cursor Mongo cursor instance to fetch data from. + * @param boolean $all whether to fetch all rows or only first one. + * @param string|callable $indexBy value to index by. + * @return array|boolean result. + * @see Query::fetchRows() + */ + protected function fetchRowsInternal($cursor, $all, $indexBy) + { + $result = []; + if ($all) { + foreach ($cursor as $file) { + $row = $file->file; + $row['file'] = $file; + if ($indexBy !== null) { + if (is_string($indexBy)) { + $key = $row[$indexBy]; + } else { + $key = call_user_func($indexBy, $row); + } + $result[$key] = $row; + } else { + $result[] = $row; + } + } + } else { + if ($cursor->hasNext()) { + $file = $cursor->getNext(); + $result = $file->file; + $result['file'] = $file; + } else { + $result = false; + } + } + + return $result; + } } diff --git a/extensions/redis/ActiveQuery.php b/extensions/redis/ActiveQuery.php index 54aec1f1bdf..7fc0a3267ab 100644 --- a/extensions/redis/ActiveQuery.php +++ b/extensions/redis/ActiveQuery.php @@ -72,374 +72,382 @@ */ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface { - use QueryTrait; - use ActiveQueryTrait; - use ActiveRelationTrait; - - /** - * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - // TODO add support for orderBy - $data = $this->executeScript($db, 'All'); - $rows = []; - foreach ($data as $dataRow) { - $row = []; - $c = count($dataRow); - for ($i = 0; $i < $c;) { - $row[$dataRow[$i++]] = $dataRow[$i++]; - } - $rows[] = $row; - } - if (!empty($rows)) { - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - return $models; - } else { - return []; - } - } - - /** - * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - // TODO add support for orderBy - $data = $this->executeScript($db, 'One'); - if (empty($data)) { - return null; - } - $row = []; - $c = count($data); - for ($i = 0; $i < $c;) { - $row[$data[$i++]] = $data[$i++]; - } - if ($this->asArray) { - $model = $row; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - if (!$this->asArray) { - $model->afterFind(); - } - return $model; - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. This parameter is ignored by this implementation. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null) - { - if ($this->where === null) { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } - return $db->executeCommand('LLEN', [$modelClass::keyPrefix()]); - } else { - return $this->executeScript($db, 'Count'); - } - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - return $this->one($db) !== null; - } - - /** - * Executes the query and returns the first column of the result. - * @param string $column name of the column to select - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($column, $db = null) - { - // TODO add support for orderBy - return $this->executeScript($db, 'Column', $column); - } - - /** - * Returns the number of records. - * @param string $column the column to sum up - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer number of records - */ - public function sum($column, $db = null) - { - return $this->executeScript($db, 'Sum', $column); - } - - /** - * Returns the average of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer the average of the specified column values. - */ - public function average($column, $db = null) - { - return $this->executeScript($db, 'Average', $column); - } - - /** - * Returns the minimum of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer the minimum of the specified column values. - */ - public function min($column, $db = null) - { - return $this->executeScript($db, 'Min', $column); - } - - /** - * Returns the maximum of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer the maximum of the specified column values. - */ - public function max($column, $db = null) - { - return $this->executeScript($db, 'Max', $column); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the specified attribute in the first record of the query results. - * @param string $attribute name of the attribute to select - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return string the value of the specified attribute in the first record of the query result. - * Null is returned if the query result is empty. - */ - public function scalar($attribute, $db = null) - { - $record = $this->one($db); - if ($record !== null) { - return $record->hasAttribute($attribute) ? $record->$attribute : null; - } else { - return null; - } - } - - - /** - * Executes a script created by [[LuaScriptBuilder]] - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @param string $type the type of the script to generate - * @param string $columnName - * @return array|bool|null|string - */ - protected function executeScript($db, $type, $columnName = null) - { - if ($this->primaryModel !== null) { - // lazy loading - if ($this->via instanceof self) { - // via pivot table - $viaModels = $this->via->findPivotRows([$this->primaryModel]); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveQuery $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - } - - if (!empty($this->orderBy)) { - throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); - } - - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - - if ($db === null) { - $db = $modelClass::getDb(); - } - - // find by primary key if possible. This is much faster than scanning all records - if (is_array($this->where) && !isset($this->where[0]) && $modelClass::isPrimaryKey(array_keys($this->where))) { - return $this->findByPk($db, $type, $columnName); - } - - $method = 'build' . $type; - $script = $db->getLuaScriptBuilder()->$method($this, $columnName); - return $db->executeCommand('EVAL', [$script, 0]); - } - - /** - * Fetch by pk if possible as this is much faster - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @param string $type the type of the script to generate - * @param string $columnName - * @return array|bool|null|string - * @throws \yii\base\InvalidParamException - * @throws \yii\base\NotSupportedException - */ - private function findByPk($db, $type, $columnName = null) - { - if (count($this->where) == 1) { - $pks = (array) reset($this->where); - } else { - foreach ($this->where as $values) { - if (is_array($values)) { - // TODO support composite IN for composite PK - throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.'); - } - } - $pks = [$this->where]; - } - - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - - if ($type == 'Count') { - $start = 0; - $limit = null; - } else { - $start = $this->offset === null ? 0 : $this->offset; - $limit = $this->limit; - } - $i = 0; - $data = []; - foreach ($pks as $pk) { - if (++$i > $start && ($limit === null || $i <= $start + $limit)) { - $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk); - $result = $db->executeCommand('HGETALL', [$key]); - if (!empty($result)) { - $data[] = $result; - if ($type === 'One' && $this->orderBy === null) { - break; - } - } - } - } - // TODO support orderBy - - switch($type) { - case 'All': - return $data; - case 'One': - return reset($data); - case 'Count': - return count($data); - case 'Column': - $column = []; - foreach ($data as $dataRow) { - $row = []; - $c = count($dataRow); - for ($i = 0; $i < $c;) { - $row[$dataRow[$i++]] = $dataRow[$i++]; - } - $column[] = $row[$columnName]; - } - return $column; - case 'Sum': - $sum = 0; - foreach ($data as $dataRow) { - $c = count($dataRow); - for ($i = 0; $i < $c;) { - if ($dataRow[$i++] == $columnName) { - $sum += $dataRow[$i]; - break; - } - } - } - return $sum; - case 'Average': - $sum = 0; - $count = 0; - foreach ($data as $dataRow) { - $count++; - $c = count($dataRow); - for ($i = 0; $i < $c;) { - if ($dataRow[$i++] == $columnName) { - $sum += $dataRow[$i]; - break; - } - } - } - return $sum / $count; - case 'Min': - $min = null; - foreach ($data as $dataRow) { - $c = count($dataRow); - for ($i = 0; $i < $c;) { - if ($dataRow[$i++] == $columnName && ($min == null || $dataRow[$i] < $min)) { - $min = $dataRow[$i]; - break; - } - } - } - return $min; - case 'Max': - $max = null; - foreach ($data as $dataRow) { - $c = count($dataRow); - for ($i = 0; $i < $c;) { - if ($dataRow[$i++] == $columnName && ($max == null || $dataRow[$i] > $max)) { - $max = $dataRow[$i]; - break; - } - } - } - return $max; - } - throw new InvalidParamException('Unknown fetch type: ' . $type); - } + use QueryTrait; + use ActiveQueryTrait; + use ActiveRelationTrait; + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + // TODO add support for orderBy + $data = $this->executeScript($db, 'All'); + $rows = []; + foreach ($data as $dataRow) { + $row = []; + $c = count($dataRow); + for ($i = 0; $i < $c;) { + $row[$dataRow[$i++]] = $dataRow[$i++]; + } + $rows[] = $row; + } + if (!empty($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } + + return $models; + } else { + return []; + } + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + // TODO add support for orderBy + $data = $this->executeScript($db, 'One'); + if (empty($data)) { + return null; + } + $row = []; + $c = count($data); + for ($i = 0; $i < $c;) { + $row[$data[$i++]] = $data[$i++]; + } + if ($this->asArray) { + $model = $row; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + if (!$this->asArray) { + $model->afterFind(); + } + + return $model; + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. This parameter is ignored by this implementation. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null) + { + if ($this->where === null) { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } + + return $db->executeCommand('LLEN', [$modelClass::keyPrefix()]); + } else { + return $this->executeScript($db, 'Count'); + } + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + return $this->one($db) !== null; + } + + /** + * Executes the query and returns the first column of the result. + * @param string $column name of the column to select + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($column, $db = null) + { + // TODO add support for orderBy + return $this->executeScript($db, 'Column', $column); + } + + /** + * Returns the number of records. + * @param string $column the column to sum up + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function sum($column, $db = null) + { + return $this->executeScript($db, 'Sum', $column); + } + + /** + * Returns the average of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($column, $db = null) + { + return $this->executeScript($db, 'Average', $column); + } + + /** + * Returns the minimum of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($column, $db = null) + { + return $this->executeScript($db, 'Min', $column); + } + + /** + * Returns the maximum of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($column, $db = null) + { + return $this->executeScript($db, 'Max', $column); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the specified attribute in the first record of the query results. + * @param string $attribute name of the attribute to select + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return string the value of the specified attribute in the first record of the query result. + * Null is returned if the query result is empty. + */ + public function scalar($attribute, $db = null) + { + $record = $this->one($db); + if ($record !== null) { + return $record->hasAttribute($attribute) ? $record->$attribute : null; + } else { + return null; + } + } + + /** + * Executes a script created by [[LuaScriptBuilder]] + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @param string $type the type of the script to generate + * @param string $columnName + * @return array|bool|null|string + */ + protected function executeScript($db, $type, $columnName = null) + { + if ($this->primaryModel !== null) { + // lazy loading + if ($this->via instanceof self) { + // via pivot table + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveQuery $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); + } else { + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; + } + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); + } + } + + if (!empty($this->orderBy)) { + throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); + } + + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + + if ($db === null) { + $db = $modelClass::getDb(); + } + + // find by primary key if possible. This is much faster than scanning all records + if (is_array($this->where) && !isset($this->where[0]) && $modelClass::isPrimaryKey(array_keys($this->where))) { + return $this->findByPk($db, $type, $columnName); + } + + $method = 'build' . $type; + $script = $db->getLuaScriptBuilder()->$method($this, $columnName); + + return $db->executeCommand('EVAL', [$script, 0]); + } + + /** + * Fetch by pk if possible as this is much faster + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @param string $type the type of the script to generate + * @param string $columnName + * @return array|bool|null|string + * @throws \yii\base\InvalidParamException + * @throws \yii\base\NotSupportedException + */ + private function findByPk($db, $type, $columnName = null) + { + if (count($this->where) == 1) { + $pks = (array) reset($this->where); + } else { + foreach ($this->where as $values) { + if (is_array($values)) { + // TODO support composite IN for composite PK + throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.'); + } + } + $pks = [$this->where]; + } + + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + + if ($type == 'Count') { + $start = 0; + $limit = null; + } else { + $start = $this->offset === null ? 0 : $this->offset; + $limit = $this->limit; + } + $i = 0; + $data = []; + foreach ($pks as $pk) { + if (++$i > $start && ($limit === null || $i <= $start + $limit)) { + $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk); + $result = $db->executeCommand('HGETALL', [$key]); + if (!empty($result)) { + $data[] = $result; + if ($type === 'One' && $this->orderBy === null) { + break; + } + } + } + } + // TODO support orderBy + + switch ($type) { + case 'All': + return $data; + case 'One': + return reset($data); + case 'Count': + return count($data); + case 'Column': + $column = []; + foreach ($data as $dataRow) { + $row = []; + $c = count($dataRow); + for ($i = 0; $i < $c;) { + $row[$dataRow[$i++]] = $dataRow[$i++]; + } + $column[] = $row[$columnName]; + } + + return $column; + case 'Sum': + $sum = 0; + foreach ($data as $dataRow) { + $c = count($dataRow); + for ($i = 0; $i < $c;) { + if ($dataRow[$i++] == $columnName) { + $sum += $dataRow[$i]; + break; + } + } + } + + return $sum; + case 'Average': + $sum = 0; + $count = 0; + foreach ($data as $dataRow) { + $count++; + $c = count($dataRow); + for ($i = 0; $i < $c;) { + if ($dataRow[$i++] == $columnName) { + $sum += $dataRow[$i]; + break; + } + } + } + + return $sum / $count; + case 'Min': + $min = null; + foreach ($data as $dataRow) { + $c = count($dataRow); + for ($i = 0; $i < $c;) { + if ($dataRow[$i++] == $columnName && ($min == null || $dataRow[$i] < $min)) { + $min = $dataRow[$i]; + break; + } + } + } + + return $min; + case 'Max': + $max = null; + foreach ($data as $dataRow) { + $c = count($dataRow); + for ($i = 0; $i < $c;) { + if ($dataRow[$i++] == $columnName && ($max == null || $dataRow[$i] > $max)) { + $max = $dataRow[$i]; + break; + } + } + } + + return $max; + } + throw new InvalidParamException('Unknown fetch type: ' . $type); + } } diff --git a/extensions/redis/ActiveRecord.php b/extensions/redis/ActiveRecord.php index 4a9050fe28a..980265a95ff 100644 --- a/extensions/redis/ActiveRecord.php +++ b/extensions/redis/ActiveRecord.php @@ -37,282 +37,291 @@ */ class ActiveRecord extends BaseActiveRecord { - /** - * Returns the database connection used by this AR class. - * By default, the "redis" application component is used as the database connection. - * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->getComponent('redis'); - } + /** + * Returns the database connection used by this AR class. + * By default, the "redis" application component is used as the database connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getComponent('redis'); + } - /** - * Creates an [[ActiveQuery]] instance. - * - * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also - * by [[hasOne()]] and [[hasMany()]] to create a relational query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ActiveQuery($config); - } + /** + * Creates an [[ActiveQuery]] instance. + * + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also + * by [[hasOne()]] and [[hasMany()]] to create a relational query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); - /** - * Returns the primary key name(s) for this AR class. - * This method should be overridden by child classes to define the primary key. - * - * Note that an array should be returned even when it is a single primary key. - * - * @return string[] the primary keys of this record. - */ - public static function primaryKey() - { - return ['id']; - } + return new ActiveQuery($config); + } - /** - * Returns the list of all attribute names of the model. - * This method must be overridden by child classes to define available attributes. - * @return array list of attribute names. - */ - public function attributes() - { - throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.'); - } + /** + * Returns the primary key name(s) for this AR class. + * This method should be overridden by child classes to define the primary key. + * + * Note that an array should be returned even when it is a single primary key. + * + * @return string[] the primary keys of this record. + */ + public static function primaryKey() + { + return ['id']; + } - /** - * Declares prefix of the key that represents the keys that store this records in redis. - * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]. - * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes - * 'order_item'. You may override this method if you want different key naming. - * @return string the prefix to apply to all AR keys - */ - public static function keyPrefix() - { - return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); - } + /** + * Returns the list of all attribute names of the model. + * This method must be overridden by child classes to define available attributes. + * @return array list of attribute names. + */ + public function attributes() + { + throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.'); + } - /** - * @inheritdoc - */ - public function insert($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - if ($this->beforeSave(true)) { - $db = static::getDb(); - $values = $this->getDirtyAttributes($attributes); - $pk = []; + /** + * Declares prefix of the key that represents the keys that store this records in redis. + * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]. + * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes + * 'order_item'. You may override this method if you want different key naming. + * @return string the prefix to apply to all AR keys + */ + public static function keyPrefix() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); + } + + /** + * @inheritdoc + */ + public function insert($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + if ($this->beforeSave(true)) { + $db = static::getDb(); + $values = $this->getDirtyAttributes($attributes); + $pk = []; // if ($values === []) { - foreach ($this->primaryKey() as $key) { - $pk[$key] = $values[$key] = $this->getAttribute($key); - if ($pk[$key] === null) { - $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::keyPrefix() . ':s:' . $key]); - $this->setAttribute($key, $values[$key]); - } - } + foreach ($this->primaryKey() as $key) { + $pk[$key] = $values[$key] = $this->getAttribute($key); + if ($pk[$key] === null) { + $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::keyPrefix() . ':s:' . $key]); + $this->setAttribute($key, $values[$key]); + } + } // } - // save pk in a findall pool - $db->executeCommand('RPUSH', [static::keyPrefix(), static::buildKey($pk)]); + // save pk in a findall pool + $db->executeCommand('RPUSH', [static::keyPrefix(), static::buildKey($pk)]); + + $key = static::keyPrefix() . ':a:' . static::buildKey($pk); + // save attributes + $args = [$key]; + foreach ($values as $attribute => $value) { + $args[] = $attribute; + $args[] = $value; + } + $db->executeCommand('HMSET', $args); + + $this->setOldAttributes($values); + $this->afterSave(true); + + return true; + } + + return false; + } + + /** + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], ['id' => 2]); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = null) + { + if (empty($attributes)) { + return 0; + } + $db = static::getDb(); + $n = 0; + foreach (static::fetchPks($condition) as $pk) { + $newPk = $pk; + $pk = static::buildKey($pk); + $key = static::keyPrefix() . ':a:' . $pk; + // save attributes + $args = [$key]; + foreach ($attributes as $attribute => $value) { + if (isset($newPk[$attribute])) { + $newPk[$attribute] = $value; + } + $args[] = $attribute; + $args[] = $value; + } + $newPk = static::buildKey($newPk); + $newKey = static::keyPrefix() . ':a:' . $newPk; + // rename index if pk changed + if ($newPk != $pk) { + $db->executeCommand('MULTI'); + $db->executeCommand('HMSET', $args); + $db->executeCommand('LINSERT', [static::keyPrefix(), 'AFTER', $pk, $newPk]); + $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]); + $db->executeCommand('RENAME', [$key, $newKey]); + $db->executeCommand('EXEC'); + } else { + $db->executeCommand('HMSET', $args); + } + $n++; + } + + return $n; + } + + /** + * Updates the whole table using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = null) + { + if (empty($counters)) { + return 0; + } + $db = static::getDb(); + $n = 0; + foreach (static::fetchPks($condition) as $pk) { + $key = static::keyPrefix() . ':a:' . static::buildKey($pk); + foreach ($counters as $attribute => $value) { + $db->executeCommand('HINCRBY', [$key, $attribute, $value]); + } + $n++; + } + + return $n; + } - $key = static::keyPrefix() . ':a:' . static::buildKey($pk); - // save attributes - $args = [$key]; - foreach ($values as $attribute => $value) { - $args[] = $attribute; - $args[] = $value; - } - $db->executeCommand('HMSET', $args); + /** + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll(['status' => 3]); + * ~~~ + * + * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = null) + { + $db = static::getDb(); + $attributeKeys = []; + $pks = static::fetchPks($condition); + $db->executeCommand('MULTI'); + foreach ($pks as $pk) { + $pk = static::buildKey($pk); + $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]); + $attributeKeys[] = static::keyPrefix() . ':a:' . $pk; + } + if (empty($attributeKeys)) { + $db->executeCommand('EXEC'); - $this->setOldAttributes($values); - $this->afterSave(true); - return true; - } - return false; - } + return 0; + } + $db->executeCommand('DEL', $attributeKeys); + $result = $db->executeCommand('EXEC'); - /** - * Updates the whole table using the provided attribute values and conditions. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], ['id' => 2]); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = null) - { - if (empty($attributes)) { - return 0; - } - $db = static::getDb(); - $n = 0; - foreach (static::fetchPks($condition) as $pk) { - $newPk = $pk; - $pk = static::buildKey($pk); - $key = static::keyPrefix() . ':a:' . $pk; - // save attributes - $args = [$key]; - foreach ($attributes as $attribute => $value) { - if (isset($newPk[$attribute])) { - $newPk[$attribute] = $value; - } - $args[] = $attribute; - $args[] = $value; - } - $newPk = static::buildKey($newPk); - $newKey = static::keyPrefix() . ':a:' . $newPk; - // rename index if pk changed - if ($newPk != $pk) { - $db->executeCommand('MULTI'); - $db->executeCommand('HMSET', $args); - $db->executeCommand('LINSERT', [static::keyPrefix(), 'AFTER', $pk, $newPk]); - $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]); - $db->executeCommand('RENAME', [$key, $newKey]); - $db->executeCommand('EXEC'); - } else { - $db->executeCommand('HMSET', $args); - } - $n++; - } - return $n; - } + return end($result); + } - /** - * Updates the whole table using the provided counter changes and conditions. - * For example, to increment all customers' age by 1, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @return integer the number of rows updated - */ - public static function updateAllCounters($counters, $condition = null) - { - if (empty($counters)) { - return 0; - } - $db = static::getDb(); - $n = 0; - foreach (static::fetchPks($condition) as $pk) { - $key = static::keyPrefix() . ':a:' . static::buildKey($pk); - foreach ($counters as $attribute => $value) { - $db->executeCommand('HINCRBY', [$key, $attribute, $value]); - } - $n++; - } - return $n; - } + private static function fetchPks($condition) + { + $query = static::createQuery(); + $query->where($condition); + $records = $query->asArray()->all(); // TODO limit fetched columns to pk + $primaryKey = static::primaryKey(); - /** - * Deletes rows in the table using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll(['status' => 3]); - * ~~~ - * - * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = null) - { - $db = static::getDb(); - $attributeKeys = []; - $pks = static::fetchPks($condition); - $db->executeCommand('MULTI'); - foreach ($pks as $pk) { - $pk = static::buildKey($pk); - $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]); - $attributeKeys[] = static::keyPrefix() . ':a:' . $pk; - } - if (empty($attributeKeys)) { - $db->executeCommand('EXEC'); - return 0; - } - $db->executeCommand('DEL', $attributeKeys); - $result = $db->executeCommand('EXEC'); - return end($result); - } + $pks = []; + foreach ($records as $record) { + $pk = []; + foreach ($primaryKey as $key) { + $pk[$key] = $record[$key]; + } + $pks[] = $pk; + } - private static function fetchPks($condition) - { - $query = static::createQuery(); - $query->where($condition); - $records = $query->asArray()->all(); // TODO limit fetched columns to pk - $primaryKey = static::primaryKey(); + return $pks; + } - $pks = []; - foreach ($records as $record) { - $pk = []; - foreach ($primaryKey as $key) { - $pk[$key] = $record[$key]; - } - $pks[] = $pk; - } - return $pks; - } + /** + * Builds a normalized key from a given primary key value. + * + * @param mixed $key the key to be normalized + * @return string the generated key + */ + public static function buildKey($key) + { + if (is_numeric($key)) { + return $key; + } elseif (is_string($key)) { + return ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key); + } elseif (is_array($key)) { + if (count($key) == 1) { + return self::buildKey(reset($key)); + } + ksort($key); // ensure order is always the same + $isNumeric = true; + foreach ($key as $value) { + if (!is_numeric($value)) { + $isNumeric = false; + } + } + if ($isNumeric) { + return implode('-', $key); + } + } - /** - * Builds a normalized key from a given primary key value. - * - * @param mixed $key the key to be normalized - * @return string the generated key - */ - public static function buildKey($key) - { - if (is_numeric($key)) { - return $key; - } elseif (is_string($key)) { - return ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key); - } elseif (is_array($key)) { - if (count($key) == 1) { - return self::buildKey(reset($key)); - } - ksort($key); // ensure order is always the same - $isNumeric = true; - foreach ($key as $value) { - if (!is_numeric($value)) { - $isNumeric = false; - } - } - if ($isNumeric) { - return implode('-', $key); - } - } - return md5(json_encode($key)); - } + return md5(json_encode($key)); + } } diff --git a/extensions/redis/Cache.php b/extensions/redis/Cache.php index 18ac8d7aa67..02438ae1cbe 100644 --- a/extensions/redis/Cache.php +++ b/extensions/redis/Cache.php @@ -58,147 +58,150 @@ */ class Cache extends \yii\caching\Cache { - /** - * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. - * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure - * redis connection as an application component. - * After the Cache object is created, if you want to change this property, you should only assign it - * with a Redis [[Connection]] object. - */ - public $redis = 'redis'; - - - /** - * Initializes the redis Cache component. - * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. - * @throws InvalidConfigException if [[redis]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->redis)) { - $this->redis = Yii::$app->getComponent($this->redis); - } elseif (is_array($this->redis)) { - if (!isset($this->redis['class'])) { - $this->redis['class'] = Connection::className(); - } - $this->redis = Yii::createObject($this->redis); - } - if (!$this->redis instanceof Connection) { - throw new InvalidConfigException("Cache::redis must be either a Redis connection instance or the application component ID of a Redis connection."); - } - } - - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - return (bool) $this->redis->executeCommand('EXISTS', [$this->buildKey($key)]); - } - - /** - * @inheritdoc - */ - protected function getValue($key) - { - return $this->redis->executeCommand('GET', [$key]); - } - - /** - * @inheritdoc - */ - protected function getValues($keys) - { - $response = $this->redis->executeCommand('MGET', $keys); - $result = []; - $i = 0; - foreach ($keys as $key) { - $result[$key] = $response[$i++]; - } - return $result; - } - - /** - * @inheritdoc - */ - protected function setValue($key, $value, $expire) - { - if ($expire == 0) { - return (bool) $this->redis->executeCommand('SET', [$key, $value]); - } else { - $expire = (int) ($expire * 1000); - return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire]); - } - } - - /** - * @inheritdoc - */ - protected function setValues($data, $expire) - { - $args = []; - foreach ($data as $key => $value) { - $args[] = $key; - $args[] = $value; - } - - $failedKeys = []; - if ($expire == 0) { - $this->redis->executeCommand('MSET', $args); - } else { - $expire = (int) ($expire * 1000); - $this->redis->executeCommand('MULTI'); - $this->redis->executeCommand('MSET', $args); - $index = []; - foreach ($data as $key => $value) { - $this->redis->executeCommand('PEXPIRE', [$key, $expire]); - $index[] = $key; - } - $result = $this->redis->executeCommand('EXEC'); - array_shift($result); - foreach ($result as $i => $r) { - if ($r != 1) { - $failedKeys[] = $index[$i]; - } - } - } - return $failedKeys; - } - - /** - * @inheritdoc - */ - protected function addValue($key, $value, $expire) - { - if ($expire == 0) { - return (bool) $this->redis->executeCommand('SET', [$key, $value, 'NX']); - } else { - $expire = (int) ($expire * 1000); - return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire, 'NX']); - } - } - - /** - * @inheritdoc - */ - protected function deleteValue($key) - { - return (bool) $this->redis->executeCommand('DEL', [$key]); - } - - /** - * @inheritdoc - */ - protected function flushValues() - { - return $this->redis->executeCommand('FLUSHDB'); - } + /** + * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. + * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure + * redis connection as an application component. + * After the Cache object is created, if you want to change this property, you should only assign it + * with a Redis [[Connection]] object. + */ + public $redis = 'redis'; + + /** + * Initializes the redis Cache component. + * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. + * @throws InvalidConfigException if [[redis]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->redis)) { + $this->redis = Yii::$app->getComponent($this->redis); + } elseif (is_array($this->redis)) { + if (!isset($this->redis['class'])) { + $this->redis['class'] = Connection::className(); + } + $this->redis = Yii::createObject($this->redis); + } + if (!$this->redis instanceof Connection) { + throw new InvalidConfigException("Cache::redis must be either a Redis connection instance or the application component ID of a Redis connection."); + } + } + + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + return (bool) $this->redis->executeCommand('EXISTS', [$this->buildKey($key)]); + } + + /** + * @inheritdoc + */ + protected function getValue($key) + { + return $this->redis->executeCommand('GET', [$key]); + } + + /** + * @inheritdoc + */ + protected function getValues($keys) + { + $response = $this->redis->executeCommand('MGET', $keys); + $result = []; + $i = 0; + foreach ($keys as $key) { + $result[$key] = $response[$i++]; + } + + return $result; + } + + /** + * @inheritdoc + */ + protected function setValue($key, $value, $expire) + { + if ($expire == 0) { + return (bool) $this->redis->executeCommand('SET', [$key, $value]); + } else { + $expire = (int) ($expire * 1000); + + return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire]); + } + } + + /** + * @inheritdoc + */ + protected function setValues($data, $expire) + { + $args = []; + foreach ($data as $key => $value) { + $args[] = $key; + $args[] = $value; + } + + $failedKeys = []; + if ($expire == 0) { + $this->redis->executeCommand('MSET', $args); + } else { + $expire = (int) ($expire * 1000); + $this->redis->executeCommand('MULTI'); + $this->redis->executeCommand('MSET', $args); + $index = []; + foreach ($data as $key => $value) { + $this->redis->executeCommand('PEXPIRE', [$key, $expire]); + $index[] = $key; + } + $result = $this->redis->executeCommand('EXEC'); + array_shift($result); + foreach ($result as $i => $r) { + if ($r != 1) { + $failedKeys[] = $index[$i]; + } + } + } + + return $failedKeys; + } + + /** + * @inheritdoc + */ + protected function addValue($key, $value, $expire) + { + if ($expire == 0) { + return (bool) $this->redis->executeCommand('SET', [$key, $value, 'NX']); + } else { + $expire = (int) ($expire * 1000); + + return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire, 'NX']); + } + } + + /** + * @inheritdoc + */ + protected function deleteValue($key) + { + return (bool) $this->redis->executeCommand('DEL', [$key]); + } + + /** + * @inheritdoc + */ + protected function flushValues() + { + return $this->redis->executeCommand('FLUSHDB'); + } } diff --git a/extensions/redis/Connection.php b/extensions/redis/Connection.php index c7296bcd986..44bbe58fc91 100644 --- a/extensions/redis/Connection.php +++ b/extensions/redis/Connection.php @@ -35,371 +35,374 @@ */ class Connection extends Component { - /** - * @event Event an event that is triggered after a DB connection is established - */ - const EVENT_AFTER_OPEN = 'afterOpen'; + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; - /** - * @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'. - */ - public $hostname = 'localhost'; - /** - * @var integer the port to use for connecting to the redis server. Default port is 6379. - */ - public $port = 6379; - /** - * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send. - * See http://redis.io/commands/auth - */ - public $password; - /** - * @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0. - */ - public $database = 0; - /** - * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") - */ - public $connectionTimeout = null; - /** - * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. - */ - public $dataTimeout = null; + /** + * @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'. + */ + public $hostname = 'localhost'; + /** + * @var integer the port to use for connecting to the redis server. Default port is 6379. + */ + public $port = 6379; + /** + * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send. + * See http://redis.io/commands/auth + */ + public $password; + /** + * @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0. + */ + public $database = 0; + /** + * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + */ + public $connectionTimeout = null; + /** + * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. + */ + public $dataTimeout = null; - /** - * @var array List of available redis commands http://redis.io/commands - */ - public $redisCommands = [ - 'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available - 'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available - 'CLIENT KILL', // ip:port Kill the connection of a client - 'CLIENT LIST', // Get the list of client connections - 'CLIENT GETNAME', // Get the current connection name - 'CLIENT SETNAME', // connection-name Set the current connection name - 'CONFIG GET', // parameter Get the value of a configuration parameter - 'CONFIG SET', // parameter value Set a configuration parameter to the given value - 'CONFIG RESETSTAT', // Reset the stats returned by INFO - 'DBSIZE', // Return the number of keys in the selected database - 'DEBUG OBJECT', // key Get debugging information about a key - 'DEBUG SEGFAULT', // Make the server crash - 'DECR', // key Decrement the integer value of a key by one - 'DECRBY', // key decrement Decrement the integer value of a key by the given number - 'DEL', // key [key ...] Delete a key - 'DISCARD', // Discard all commands issued after MULTI - 'DUMP', // key Return a serialized version of the value stored at the specified key. - 'ECHO', // message Echo the given string - 'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side - 'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side - 'EXEC', // Execute all commands issued after MULTI - 'EXISTS', // key Determine if a key exists - 'EXPIRE', // key seconds Set a key's time to live in seconds - 'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp - 'FLUSHALL', // Remove all keys from all databases - 'FLUSHDB', // Remove all keys from the current database - 'GET', // key Get the value of a key - 'GETBIT', // key offset Returns the bit value at offset in the string value stored at key - 'GETRANGE', // key start end Get a substring of the string stored at a key - 'GETSET', // key value Set the string value of a key and return its old value - 'HDEL', // key field [field ...] Delete one or more hash fields - 'HEXISTS', // key field Determine if a hash field exists - 'HGET', // key field Get the value of a hash field - 'HGETALL', // key Get all the fields and values in a hash - 'HINCRBY', // key field increment Increment the integer value of a hash field by the given number - 'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount - 'HKEYS', // key Get all the fields in a hash - 'HLEN', // key Get the number of fields in a hash - 'HMGET', // key field [field ...] Get the values of all the given hash fields - 'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values - 'HSET', // key field value Set the string value of a hash field - 'HSETNX', // key field value Set the value of a hash field, only if the field does not exist - 'HVALS', // key Get all the values in a hash - 'INCR', // key Increment the integer value of a key by one - 'INCRBY', // key increment Increment the integer value of a key by the given amount - 'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount - 'INFO', // [section] Get information and statistics about the server - 'KEYS', // pattern Find all keys matching the given pattern - 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk - 'LINDEX', // key index Get an element from a list by its index - 'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list - 'LLEN', // key Get the length of a list - 'LPOP', // key Remove and get the first element in a list - 'LPUSH', // key value [value ...] Prepend one or multiple values to a list - 'LPUSHX', // key value Prepend a value to a list, only if the list exists - 'LRANGE', // key start stop Get a range of elements from a list - 'LREM', // key count value Remove elements from a list - 'LSET', // key index value Set the value of an element in a list by its index - 'LTRIM', // key start stop Trim a list to the specified range - 'MGET', // key [key ...] Get the values of all the given keys - 'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one. - 'MONITOR', // Listen for all requests received by the server in real time - 'MOVE', // key db Move a key to another database - 'MSET', // key value [key value ...] Set multiple keys to multiple values - 'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist - 'MULTI', // Mark the start of a transaction block - 'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects - 'PERSIST', // key Remove the expiration from a key - 'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds - 'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds - 'PING', // Ping the server - 'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key - 'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns - 'PTTL', // key Get the time to live for a key in milliseconds - 'PUBLISH', // channel message Post a message to a channel - 'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns - 'QUIT', // Close the connection - 'RANDOMKEY', // Return a random key from the keyspace - 'RENAME', // key newkey Rename a key - 'RENAMENX', // key newkey Rename a key, only if the new key does not exist - 'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP. - 'RPOP', // key Remove and get the last element in a list - 'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it - 'RPUSH', // key value [value ...] Append one or multiple values to a list - 'RPUSHX', // key value Append a value to a list, only if the list exists - 'SADD', // key member [member ...] Add one or more members to a set - 'SAVE', // Synchronously save the dataset to disk - 'SCARD', // key Get the number of members in a set - 'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache. - 'SCRIPT FLUSH', // Remove all the scripts from the script cache. - 'SCRIPT KILL', // Kill the script currently in execution. - 'SCRIPT LOAD', // script Load the specified Lua script into the script cache. - 'SDIFF', // key [key ...] Subtract multiple sets - 'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key - 'SELECT', // index Change the selected database for the current connection - 'SET', // key value Set the string value of a key - 'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key - 'SETEX', // key seconds value Set the value and expiration of a key - 'SETNX', // key value Set the value of a key, only if the key does not exist - 'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset - 'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server - 'SINTER', // key [key ...] Intersect multiple sets - 'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key - 'SISMEMBER', // key member Determine if a given value is a member of a set - 'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master - 'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log - 'SMEMBERS', // key Get all the members in a set - 'SMOVE', // source destination member Move a member from one set to another - 'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set - 'SPOP', // key Remove and return a random member from a set - 'SRANDMEMBER', // key [count] Get one or multiple random members from a set - 'SREM', // key member [member ...] Remove one or more members from a set - 'STRLEN', // key Get the length of the value stored in a key - 'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels - 'SUNION', // key [key ...] Add multiple sets - 'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key - 'SYNC', // Internal command used for replication - 'TIME', // Return the current server time - 'TTL', // key Get the time to live for a key - 'TYPE', // key Determine the type stored at key - 'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels - 'UNWATCH', // Forget about all watched keys - 'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block - 'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists - 'ZCARD', // key Get the number of members in a sorted set - 'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values - 'ZINCRBY', // key increment member Increment the score of a member in a sorted set - 'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key - 'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index - 'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score - 'ZRANK', // key member Determine the index of a member in a sorted set - 'ZREM', // key member [member ...] Remove one or more members from a sorted set - 'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes - 'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores - 'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low - 'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low - 'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low - 'ZSCORE', // key member Get the score associated with the given member in a sorted set - 'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key - ]; - /** - * @var resource redis socket connection - */ - private $_socket; + /** + * @var array List of available redis commands http://redis.io/commands + */ + public $redisCommands = [ + 'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available + 'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available + 'CLIENT KILL', // ip:port Kill the connection of a client + 'CLIENT LIST', // Get the list of client connections + 'CLIENT GETNAME', // Get the current connection name + 'CLIENT SETNAME', // connection-name Set the current connection name + 'CONFIG GET', // parameter Get the value of a configuration parameter + 'CONFIG SET', // parameter value Set a configuration parameter to the given value + 'CONFIG RESETSTAT', // Reset the stats returned by INFO + 'DBSIZE', // Return the number of keys in the selected database + 'DEBUG OBJECT', // key Get debugging information about a key + 'DEBUG SEGFAULT', // Make the server crash + 'DECR', // key Decrement the integer value of a key by one + 'DECRBY', // key decrement Decrement the integer value of a key by the given number + 'DEL', // key [key ...] Delete a key + 'DISCARD', // Discard all commands issued after MULTI + 'DUMP', // key Return a serialized version of the value stored at the specified key. + 'ECHO', // message Echo the given string + 'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side + 'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side + 'EXEC', // Execute all commands issued after MULTI + 'EXISTS', // key Determine if a key exists + 'EXPIRE', // key seconds Set a key's time to live in seconds + 'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp + 'FLUSHALL', // Remove all keys from all databases + 'FLUSHDB', // Remove all keys from the current database + 'GET', // key Get the value of a key + 'GETBIT', // key offset Returns the bit value at offset in the string value stored at key + 'GETRANGE', // key start end Get a substring of the string stored at a key + 'GETSET', // key value Set the string value of a key and return its old value + 'HDEL', // key field [field ...] Delete one or more hash fields + 'HEXISTS', // key field Determine if a hash field exists + 'HGET', // key field Get the value of a hash field + 'HGETALL', // key Get all the fields and values in a hash + 'HINCRBY', // key field increment Increment the integer value of a hash field by the given number + 'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount + 'HKEYS', // key Get all the fields in a hash + 'HLEN', // key Get the number of fields in a hash + 'HMGET', // key field [field ...] Get the values of all the given hash fields + 'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values + 'HSET', // key field value Set the string value of a hash field + 'HSETNX', // key field value Set the value of a hash field, only if the field does not exist + 'HVALS', // key Get all the values in a hash + 'INCR', // key Increment the integer value of a key by one + 'INCRBY', // key increment Increment the integer value of a key by the given amount + 'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount + 'INFO', // [section] Get information and statistics about the server + 'KEYS', // pattern Find all keys matching the given pattern + 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk + 'LINDEX', // key index Get an element from a list by its index + 'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list + 'LLEN', // key Get the length of a list + 'LPOP', // key Remove and get the first element in a list + 'LPUSH', // key value [value ...] Prepend one or multiple values to a list + 'LPUSHX', // key value Prepend a value to a list, only if the list exists + 'LRANGE', // key start stop Get a range of elements from a list + 'LREM', // key count value Remove elements from a list + 'LSET', // key index value Set the value of an element in a list by its index + 'LTRIM', // key start stop Trim a list to the specified range + 'MGET', // key [key ...] Get the values of all the given keys + 'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one. + 'MONITOR', // Listen for all requests received by the server in real time + 'MOVE', // key db Move a key to another database + 'MSET', // key value [key value ...] Set multiple keys to multiple values + 'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist + 'MULTI', // Mark the start of a transaction block + 'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects + 'PERSIST', // key Remove the expiration from a key + 'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds + 'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds + 'PING', // Ping the server + 'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key + 'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns + 'PTTL', // key Get the time to live for a key in milliseconds + 'PUBLISH', // channel message Post a message to a channel + 'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns + 'QUIT', // Close the connection + 'RANDOMKEY', // Return a random key from the keyspace + 'RENAME', // key newkey Rename a key + 'RENAMENX', // key newkey Rename a key, only if the new key does not exist + 'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP. + 'RPOP', // key Remove and get the last element in a list + 'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it + 'RPUSH', // key value [value ...] Append one or multiple values to a list + 'RPUSHX', // key value Append a value to a list, only if the list exists + 'SADD', // key member [member ...] Add one or more members to a set + 'SAVE', // Synchronously save the dataset to disk + 'SCARD', // key Get the number of members in a set + 'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache. + 'SCRIPT FLUSH', // Remove all the scripts from the script cache. + 'SCRIPT KILL', // Kill the script currently in execution. + 'SCRIPT LOAD', // script Load the specified Lua script into the script cache. + 'SDIFF', // key [key ...] Subtract multiple sets + 'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key + 'SELECT', // index Change the selected database for the current connection + 'SET', // key value Set the string value of a key + 'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key + 'SETEX', // key seconds value Set the value and expiration of a key + 'SETNX', // key value Set the value of a key, only if the key does not exist + 'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset + 'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server + 'SINTER', // key [key ...] Intersect multiple sets + 'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key + 'SISMEMBER', // key member Determine if a given value is a member of a set + 'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master + 'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log + 'SMEMBERS', // key Get all the members in a set + 'SMOVE', // source destination member Move a member from one set to another + 'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set + 'SPOP', // key Remove and return a random member from a set + 'SRANDMEMBER', // key [count] Get one or multiple random members from a set + 'SREM', // key member [member ...] Remove one or more members from a set + 'STRLEN', // key Get the length of the value stored in a key + 'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels + 'SUNION', // key [key ...] Add multiple sets + 'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key + 'SYNC', // Internal command used for replication + 'TIME', // Return the current server time + 'TTL', // key Get the time to live for a key + 'TYPE', // key Determine the type stored at key + 'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels + 'UNWATCH', // Forget about all watched keys + 'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block + 'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists + 'ZCARD', // key Get the number of members in a sorted set + 'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values + 'ZINCRBY', // key increment member Increment the score of a member in a sorted set + 'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key + 'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index + 'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score + 'ZRANK', // key member Determine the index of a member in a sorted set + 'ZREM', // key member [member ...] Remove one or more members from a sorted set + 'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes + 'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores + 'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low + 'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low + 'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low + 'ZSCORE', // key member Get the score associated with the given member in a sorted set + 'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key + ]; + /** + * @var resource redis socket connection + */ + private $_socket; - /** - * Closes the connection when this component is being serialized. - * @return array - */ - public function __sleep() - { - $this->close(); - return array_keys(get_object_vars($this)); - } + /** + * Closes the connection when this component is being serialized. + * @return array + */ + public function __sleep() + { + $this->close(); - /** - * Returns a value indicating whether the DB connection is established. - * @return boolean whether the DB connection is established - */ - public function getIsActive() - { - return $this->_socket !== null; - } + return array_keys(get_object_vars($this)); + } - /** - * Establishes a DB connection. - * It does nothing if a DB connection has already been established. - * @throws Exception if connection fails - */ - public function open() - { - if ($this->_socket !== null) { - return; - } - $connection = $this->hostname . ':' . $this->port . ', database=' . $this->database; - \Yii::trace('Opening redis DB connection: ' . $connection, __CLASS__); - $this->_socket = @stream_socket_client( - 'tcp://' . $this->hostname . ':' . $this->port, - $errorNumber, - $errorDescription, - $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout") - ); - if ($this->_socket) { - if ($this->dataTimeout !== null) { - stream_set_timeout($this->_socket, $timeout = (int)$this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); - } - if ($this->password !== null) { - $this->executeCommand('AUTH', [$this->password]); - } - $this->executeCommand('SELECT', [$this->database]); - $this->initConnection(); - } else { - \Yii::error("Failed to open redis DB connection ($connection): $errorNumber - $errorDescription", __CLASS__); - $message = YII_DEBUG ? "Failed to open redis DB connection ($connection): $errorNumber - $errorDescription" : 'Failed to open DB connection.'; - throw new Exception($message, $errorDescription, (int)$errorNumber); - } - } + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return $this->_socket !== null; + } - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - if ($this->_socket !== null) { - $connection = $this->hostname . ':' . $this->port . ', database=' . $this->database; - \Yii::trace('Closing DB connection: ' . $connection, __CLASS__); - $this->executeCommand('QUIT'); - stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); - $this->_socket = null; - } - } + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->_socket !== null) { + return; + } + $connection = $this->hostname . ':' . $this->port . ', database=' . $this->database; + \Yii::trace('Opening redis DB connection: ' . $connection, __CLASS__); + $this->_socket = @stream_socket_client( + 'tcp://' . $this->hostname . ':' . $this->port, + $errorNumber, + $errorDescription, + $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout") + ); + if ($this->_socket) { + if ($this->dataTimeout !== null) { + stream_set_timeout($this->_socket, $timeout = (int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); + } + if ($this->password !== null) { + $this->executeCommand('AUTH', [$this->password]); + } + $this->executeCommand('SELECT', [$this->database]); + $this->initConnection(); + } else { + \Yii::error("Failed to open redis DB connection ($connection): $errorNumber - $errorDescription", __CLASS__); + $message = YII_DEBUG ? "Failed to open redis DB connection ($connection): $errorNumber - $errorDescription" : 'Failed to open DB connection.'; + throw new Exception($message, $errorDescription, (int) $errorNumber); + } + } - /** - * Initializes the DB connection. - * This method is invoked right after the DB connection is established. - * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. - */ - protected function initConnection() - { - $this->trigger(self::EVENT_AFTER_OPEN); - } + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + if ($this->_socket !== null) { + $connection = $this->hostname . ':' . $this->port . ', database=' . $this->database; + \Yii::trace('Closing DB connection: ' . $connection, __CLASS__); + $this->executeCommand('QUIT'); + stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); + $this->_socket = null; + } + } - /** - * Returns the name of the DB driver for the current [[dsn]]. - * @return string name of the DB driver - */ - public function getDriverName() - { - return 'redis'; - } + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } - /** - * @return LuaScriptBuilder - */ - public function getLuaScriptBuilder() - { - return new LuaScriptBuilder(); - } + /** + * Returns the name of the DB driver for the current [[dsn]]. + * @return string name of the DB driver + */ + public function getDriverName() + { + return 'redis'; + } - /** - * - * @param string $name - * @param array $params - * @return mixed - */ - public function __call($name, $params) - { - $redisCommand = strtoupper(Inflector::camel2words($name, false)); - if (in_array($redisCommand, $this->redisCommands)) { - return $this->executeCommand($name, $params); - } else { - return parent::__call($name, $params); - } - } + /** + * @return LuaScriptBuilder + */ + public function getLuaScriptBuilder() + { + return new LuaScriptBuilder(); + } - /** - * Executes a redis command. - * For a list of available commands and their parameters see http://redis.io/commands. - * - * @param string $name the name of the command - * @param array $params list of parameters for the command - * @return array|bool|null|string Dependend on the executed command this method - * will return different data types: - * - * - `true` for commands that return "status reply". - * - `string` for commands that return "integer reply" - * as the value is in the range of a signed 64 bit integer. - * - `string` or `null` for commands that return "bulk reply". - * - `array` for commands that return "Multi-bulk replies". - * - * See [redis protocol description](http://redis.io/topics/protocol) - * for details on the mentioned reply types. - * @trows Exception for commands that return [error reply](http://redis.io/topics/protocol#error-reply). - */ - public function executeCommand($name, $params = []) - { - $this->open(); + /** + * + * @param string $name + * @param array $params + * @return mixed + */ + public function __call($name, $params) + { + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands)) { + return $this->executeCommand($name, $params); + } else { + return parent::__call($name, $params); + } + } - array_unshift($params, $name); - $command = '*' . count($params) . "\r\n"; - foreach ($params as $arg) { - $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; - } + /** + * Executes a redis command. + * For a list of available commands and their parameters see http://redis.io/commands. + * + * @param string $name the name of the command + * @param array $params list of parameters for the command + * @return array|bool|null|string Dependend on the executed command this method + * will return different data types: + * + * - `true` for commands that return "status reply". + * - `string` for commands that return "integer reply" + * as the value is in the range of a signed 64 bit integer. + * - `string` or `null` for commands that return "bulk reply". + * - `array` for commands that return "Multi-bulk replies". + * + * See [redis protocol description](http://redis.io/topics/protocol) + * for details on the mentioned reply types. + * @trows Exception for commands that return [error reply](http://redis.io/topics/protocol#error-reply). + */ + public function executeCommand($name, $params = []) + { + $this->open(); - \Yii::trace("Executing Redis Command: {$name}", __CLASS__); - fwrite($this->_socket, $command); + array_unshift($params, $name); + $command = '*' . count($params) . "\r\n"; + foreach ($params as $arg) { + $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; + } - return $this->parseResponse(implode(' ', $params)); - } + \Yii::trace("Executing Redis Command: {$name}", __CLASS__); + fwrite($this->_socket, $command); - private function parseResponse($command) - { - if (($line = fgets($this->_socket)) === false) { - throw new Exception("Failed to read from socket.\nRedis command was: " . $command); - } - $type = $line[0]; - $line = mb_substr($line, 1, -2, '8bit'); - switch($type) - { - case '+': // Status reply - return true; - case '-': // Error reply - throw new Exception("Redis error: " . $line . "\nRedis command was: " . $command); - case ':': // Integer reply - // no cast to int as it is in the range of a signed 64 bit integer - return $line; - case '$': // Bulk replies - if ($line == '-1') { - return null; - } - $length = $line + 2; - $data = ''; - while ($length > 0) { - if (($block = fread($this->_socket, $line + 2)) === false) { - throw new Exception("Failed to read from socket.\nRedis command was: " . $command); - } - $data .= $block; - $length -= mb_strlen($block, '8bit'); - } - return mb_substr($data, 0, -2, '8bit'); - case '*': // Multi-bulk replies - $count = (int) $line; - $data = []; - for ($i = 0; $i < $count; $i++) { - $data[] = $this->parseResponse($command); - } - return $data; - default: - throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); - } - } + return $this->parseResponse(implode(' ', $params)); + } + + private function parseResponse($command) + { + if (($line = fgets($this->_socket)) === false) { + throw new Exception("Failed to read from socket.\nRedis command was: " . $command); + } + $type = $line[0]; + $line = mb_substr($line, 1, -2, '8bit'); + switch ($type) { + case '+': // Status reply + + return true; + case '-': // Error reply + throw new Exception("Redis error: " . $line . "\nRedis command was: " . $command); + case ':': // Integer reply + // no cast to int as it is in the range of a signed 64 bit integer + return $line; + case '$': // Bulk replies + if ($line == '-1') { + return null; + } + $length = $line + 2; + $data = ''; + while ($length > 0) { + if (($block = fread($this->_socket, $line + 2)) === false) { + throw new Exception("Failed to read from socket.\nRedis command was: " . $command); + } + $data .= $block; + $length -= mb_strlen($block, '8bit'); + } + + return mb_substr($data, 0, -2, '8bit'); + case '*': // Multi-bulk replies + $count = (int) $line; + $data = []; + for ($i = 0; $i < $count; $i++) { + $data[] = $this->parseResponse($command); + } + + return $data; + default: + throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); + } + } } diff --git a/extensions/redis/LuaScriptBuilder.php b/extensions/redis/LuaScriptBuilder.php index 9fd483ef0f3..68c60c410d5 100644 --- a/extensions/redis/LuaScriptBuilder.php +++ b/extensions/redis/LuaScriptBuilder.php @@ -20,147 +20,154 @@ */ class LuaScriptBuilder extends \yii\base\Object { - /** - * Builds a Lua script for finding a list of records - * @param ActiveQuery $query the query used to build the script - * @return string - */ - public function buildAll($query) - { - // TODO add support for orderBy - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL',$key .. pk)", 'pks'); - } - - /** - * Builds a Lua script for finding one record - * @param ActiveQuery $query the query used to build the script - * @return string - */ - public function buildOne($query) - { - // TODO add support for orderBy - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "do return redis.call('HGETALL',$key .. pk) end", 'pks'); - } - - /** - * Builds a Lua script for finding a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildColumn($query, $column) - { - // TODO add support for orderBy and indexBy - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks'); - } - - /** - * Builds a Lua script for getting count of records - * @param ActiveQuery $query the query used to build the script - * @return string - */ - public function buildCount($query) - { - return $this->build($query, 'n=n+1', 'n'); - } - - /** - * Builds a Lua script for finding the sum of a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildSum($query, $column) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n'); - } - - /** - * Builds a Lua script for finding the average of a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildAverage($query, $column) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n'); - } - - /** - * Builds a Lua script for finding the min value of a column - * @param ActiveQuery $query the query used to build the script - * @param string $column name of the column - * @return string - */ - public function buildMin($query, $column) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nmodelClass; - $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); - return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v'); - } - - /** - * @param ActiveQuery $query the query used to build the script - * @param string $buildResult the lua script for building the result - * @param string $return the lua variable that should be returned - * @throws yii\base\NotSupportedException when query contains unsupported order by condition - * @return string - */ - private function build($query, $buildResult, $return) - { - if (!empty($query->orderBy)) { - throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); - } - - $columns = []; - if ($query->where !== null) { - $condition = $this->buildCondition($query->where, $columns); - } else { - $condition = 'true'; - } - - $start = $query->offset === null ? 0 : $query->offset; - $limitCondition = 'i>' . $start . ($query->limit === null ? '' : ' and i<=' . ($start + $query->limit)); - - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::keyPrefix()); - $loadColumnValues = ''; - foreach ($columns as $column => $alias) { - $loadColumnValues .= "local $alias=redis.call('HGET',$key .. ':a:' .. pk, '$column')\n"; - } - - return <<modelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL',$key .. pk)", 'pks'); + } + + /** + * Builds a Lua script for finding one record + * @param ActiveQuery $query the query used to build the script + * @return string + */ + public function buildOne($query) + { + // TODO add support for orderBy + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "do return redis.call('HGETALL',$key .. pk) end", 'pks'); + } + + /** + * Builds a Lua script for finding a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildColumn($query, $column) + { + // TODO add support for orderBy and indexBy + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks'); + } + + /** + * Builds a Lua script for getting count of records + * @param ActiveQuery $query the query used to build the script + * @return string + */ + public function buildCount($query) + { + return $this->build($query, 'n=n+1', 'n'); + } + + /** + * Builds a Lua script for finding the sum of a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildSum($query, $column) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n'); + } + + /** + * Builds a Lua script for finding the average of a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildAverage($query, $column) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n'); + } + + /** + * Builds a Lua script for finding the min value of a column + * @param ActiveQuery $query the query used to build the script + * @param string $column name of the column + * @return string + */ + public function buildMin($query, $column) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nmodelClass; + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); + + return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v'); + } + + /** + * @param ActiveQuery $query the query used to build the script + * @param string $buildResult the lua script for building the result + * @param string $return the lua variable that should be returned + * @throws yii\base\NotSupportedException when query contains unsupported order by condition + * @return string + */ + private function build($query, $buildResult, $return) + { + if (!empty($query->orderBy)) { + throw new NotSupportedException('orderBy is currently not supported by redis ActiveRecord.'); + } + + $columns = []; + if ($query->where !== null) { + $condition = $this->buildCondition($query->where, $columns); + } else { + $condition = 'true'; + } + + $start = $query->offset === null ? 0 : $query->offset; + $limitCondition = 'i>' . $start . ($query->limit === null ? '' : ' and i<=' . ($start + $query->limit)); + + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $key = $this->quoteValue($modelClass::keyPrefix()); + $loadColumnValues = ''; + foreach ($columns as $column => $alias) { + $loadColumnValues .= "local $alias=redis.call('HGET',$key .. ':a:' .. pk, '$column')\n"; + } + + return << 'buildNotCondition', - 'and' => 'buildAndCondition', - 'or' => 'buildAndCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', - ]; - - if (!is_array($condition)) { - throw new NotSupportedException('Where condition must be an array in redis ActiveRecord.'); - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtolower($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition, $columns); - } else { - throw new Exception('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition, $columns); - } - } - - private function buildHashCondition($condition, &$columns) - { - $parts = []; - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', [$column, $value], $columns); - } else { - $column = $this->addColumn($column, $columns); - if ($value === null) { - $parts[] = "$column==nil"; - } elseif ($value instanceof Expression) { - $parts[] = "$column==" . $value->expression; - } else { - $value = $this->quoteValue($value); - $parts[] = "$column==$value"; - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') and (', $parts) . ')'; - } - - private function buildNotCondition($operator, $operands, &$params) - { - if (count($operands) != 1) { - throw new InvalidParamException("Operator '$operator' requires exactly one operand."); - } - - $operand = reset($operands); - if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); - } - return "!($operand)"; - } - - private function buildAndCondition($operator, $operands, &$columns) - { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand, $columns); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - private function buildBetweenCondition($operator, $operands, &$columns) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - $value1 = $this->quoteValue($value1); - $value2 = $this->quoteValue($value2); - $column = $this->addColumn($column, $columns); - return "$column >= $value1 and $column <= $value2"; - } - - private function buildInCondition($operator, $operands, &$columns) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'in' ? 'false' : 'true'; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values, $columns); - } elseif (is_array($column)) { - $column = reset($column); - } - $columnAlias = $this->addColumn($column, $columns); - $parts = []; - foreach ($values as $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $parts[] = "$columnAlias==nil"; - } elseif ($value instanceof Expression) { - $parts[] = "$columnAlias==" . $value->expression; - } else { - $value = $this->quoteValue($value); - $parts[] = "$columnAlias==$value"; - } - } - $operator = $operator === 'in' ? '' : 'not '; - return "$operator(" . implode(' or ', $parts) . ')'; - } - - protected function buildCompositeInCondition($operator, $inColumns, $values, &$columns) - { - $vss = []; - foreach ($values as $value) { - $vs = []; - foreach ($inColumns as $column) { - $column = $this->addColumn($column, $columns); - if (isset($value[$column])) { - $vs[] = "$column==" . $this->quoteValue($value[$column]); - } else { - $vs[] = "$column==nil"; - } - } - $vss[] = '(' . implode(' and ', $vs) . ')'; - } - $operator = $operator === 'in' ? '' : 'not '; - return "$operator(" . implode(' or ', $vss) . ')'; - } - - private function buildLikeCondition($operator, $operands, &$columns) - { - throw new NotSupportedException('LIKE conditions are not suppoerted by redis ActiveRecord.'); - } + } + + /** + * Adds a column to the list of columns to retrieve and creates an alias + * @param string $column the column name to add + * @param array $columns list of columns given by reference + * @return string the alias generated for the column name + */ + private function addColumn($column, &$columns) + { + if (isset($columns[$column])) { + return $columns[$column]; + } + $name = 'c' . preg_replace("/[^A-z]+/", "", $column) . count($columns); + + return $columns[$column] = $name; + } + + /** + * Quotes a string value for use in a query. + * Note that if the parameter is not a string or int, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + */ + private function quoteValue($str) + { + if (!is_string($str) && !is_int($str)) { + return $str; + } + + return "'" . addcslashes(str_replace("'", "\\'", $str), "\000\n\r\\\032") . "'"; + } + + /** + * Parses the condition specification and generates the corresponding Lua expression. + * @param string|array $condition the condition specification. Please refer to [[ActiveQuery::where()]] + * on how to specify a condition. + * @param array $columns the list of columns and aliases to be used + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + * @throws \yii\base\NotSupportedException if the condition is not an array + */ + public function buildCondition($condition, &$columns) + { + static $builders = [ + 'not' => 'buildNotCondition', + 'and' => 'buildAndCondition', + 'or' => 'buildAndCondition', + 'between' => 'buildBetweenCondition', + 'not between' => 'buildBetweenCondition', + 'in' => 'buildInCondition', + 'not in' => 'buildInCondition', + 'like' => 'buildLikeCondition', + 'not like' => 'buildLikeCondition', + 'or like' => 'buildLikeCondition', + 'or not like' => 'buildLikeCondition', + ]; + + if (!is_array($condition)) { + throw new NotSupportedException('Where condition must be an array in redis ActiveRecord.'); + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtolower($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + + return $this->$method($operator, $condition, $columns); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + + return $this->buildHashCondition($condition, $columns); + } + } + + private function buildHashCondition($condition, &$columns) + { + $parts = []; + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('in', [$column, $value], $columns); + } else { + $column = $this->addColumn($column, $columns); + if ($value === null) { + $parts[] = "$column==nil"; + } elseif ($value instanceof Expression) { + $parts[] = "$column==" . $value->expression; + } else { + $value = $this->quoteValue($value); + $parts[] = "$column==$value"; + } + } + } + + return count($parts) === 1 ? $parts[0] : '(' . implode(') and (', $parts) . ')'; + } + + private function buildNotCondition($operator, $operands, &$params) + { + if (count($operands) != 1) { + throw new InvalidParamException("Operator '$operator' requires exactly one operand."); + } + + $operand = reset($operands); + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + + return "!($operand)"; + } + + private function buildAndCondition($operator, $operands, &$columns) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $columns); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + private function buildBetweenCondition($operator, $operands, &$columns) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + $value1 = $this->quoteValue($value1); + $value2 = $this->quoteValue($value2); + $column = $this->addColumn($column, $columns); + + return "$column >= $value1 and $column <= $value2"; + } + + private function buildInCondition($operator, $operands, &$columns) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array) $values; + + if (empty($values) || $column === []) { + return $operator === 'in' ? 'false' : 'true'; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $columns); + } elseif (is_array($column)) { + $column = reset($column); + } + $columnAlias = $this->addColumn($column, $columns); + $parts = []; + foreach ($values as $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $parts[] = "$columnAlias==nil"; + } elseif ($value instanceof Expression) { + $parts[] = "$columnAlias==" . $value->expression; + } else { + $value = $this->quoteValue($value); + $parts[] = "$columnAlias==$value"; + } + } + $operator = $operator === 'in' ? '' : 'not '; + + return "$operator(" . implode(' or ', $parts) . ')'; + } + + protected function buildCompositeInCondition($operator, $inColumns, $values, &$columns) + { + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($inColumns as $column) { + $column = $this->addColumn($column, $columns); + if (isset($value[$column])) { + $vs[] = "$column==" . $this->quoteValue($value[$column]); + } else { + $vs[] = "$column==nil"; + } + } + $vss[] = '(' . implode(' and ', $vs) . ')'; + } + $operator = $operator === 'in' ? '' : 'not '; + + return "$operator(" . implode(' or ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands, &$columns) + { + throw new NotSupportedException('LIKE conditions are not suppoerted by redis ActiveRecord.'); + } } diff --git a/extensions/redis/Session.php b/extensions/redis/Session.php index a9c3bdd29cc..1d1fe7608a8 100644 --- a/extensions/redis/Session.php +++ b/extensions/redis/Session.php @@ -55,99 +55,100 @@ */ class Session extends \yii\web\Session { - /** - * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. - * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure - * redis connection as an application component. - * After the Session object is created, if you want to change this property, you should only assign it - * with a Redis [[Connection]] object. - */ - public $redis = 'redis'; - /** - * @var string a string prefixed to every cache key so that it is unique. If not set, - * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string - * if you don't want to use key prefix. It is recommended that you explicitly set this property to some - * static value if the cached data needs to be shared among multiple applications. - */ - public $keyPrefix; + /** + * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. + * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure + * redis connection as an application component. + * After the Session object is created, if you want to change this property, you should only assign it + * with a Redis [[Connection]] object. + */ + public $redis = 'redis'; + /** + * @var string a string prefixed to every cache key so that it is unique. If not set, + * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string + * if you don't want to use key prefix. It is recommended that you explicitly set this property to some + * static value if the cached data needs to be shared among multiple applications. + */ + public $keyPrefix; - /** - * Initializes the redis Session component. - * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. - * @throws InvalidConfigException if [[redis]] is invalid. - */ - public function init() - { - if (is_string($this->redis)) { - $this->redis = Yii::$app->getComponent($this->redis); - } elseif (is_array($this->redis)) { - if (!isset($this->redis['class'])) { - $this->redis['class'] = Connection::className(); - } - $this->redis = Yii::createObject($this->redis); - } - if (!$this->redis instanceof Connection) { - throw new InvalidConfigException("Session::redis must be either a Redis connection instance or the application component ID of a Redis connection."); - } - if ($this->keyPrefix === null) { - $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); - } - parent::init(); - } + /** + * Initializes the redis Session component. + * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. + * @throws InvalidConfigException if [[redis]] is invalid. + */ + public function init() + { + if (is_string($this->redis)) { + $this->redis = Yii::$app->getComponent($this->redis); + } elseif (is_array($this->redis)) { + if (!isset($this->redis['class'])) { + $this->redis['class'] = Connection::className(); + } + $this->redis = Yii::createObject($this->redis); + } + if (!$this->redis instanceof Connection) { + throw new InvalidConfigException("Session::redis must be either a Redis connection instance or the application component ID of a Redis connection."); + } + if ($this->keyPrefix === null) { + $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); + } + parent::init(); + } - /** - * Returns a value indicating whether to use custom session storage. - * This method overrides the parent implementation and always returns true. - * @return boolean whether to use custom storage. - */ - public function getUseCustomStorage() - { - return true; - } + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } - /** - * Session read handler. - * Do not call this method directly. - * @param string $id session ID - * @return string the session data - */ - public function readSession($id) - { - $data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]); - return $data === false ? '' : $data; - } + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]); - /** - * Session write handler. - * Do not call this method directly. - * @param string $id session ID - * @param string $data session data - * @return boolean whether session write is successful - */ - public function writeSession($id, $data) - { - return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]); - } + return $data === false ? '' : $data; + } - /** - * Session destroy handler. - * Do not call this method directly. - * @param string $id session ID - * @return boolean whether session is destroyed successfully - */ - public function destroySession($id) - { - return (bool) $this->redis->executeCommand('DEL', [$this->calculateKey($id)]); - } + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]); + } - /** - * Generates a unique key used for storing session data in cache. - * @param string $id session variable name - * @return string a safe cache key associated with the session variable name - */ - protected function calculateKey($id) - { - return $this->keyPrefix . md5(json_encode([__CLASS__, $id])); - } + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + return (bool) $this->redis->executeCommand('DEL', [$this->calculateKey($id)]); + } + + /** + * Generates a unique key used for storing session data in cache. + * @param string $id session variable name + * @return string a safe cache key associated with the session variable name + */ + protected function calculateKey($id) + { + return $this->keyPrefix . md5(json_encode([__CLASS__, $id])); + } } diff --git a/extensions/smarty/ViewRenderer.php b/extensions/smarty/ViewRenderer.php index 574020a93a8..0efec276ab8 100644 --- a/extensions/smarty/ViewRenderer.php +++ b/extensions/smarty/ViewRenderer.php @@ -23,76 +23,76 @@ */ class ViewRenderer extends BaseViewRenderer { - /** - * @var string the directory or path alias pointing to where Smarty cache will be stored. - */ - public $cachePath = '@runtime/Smarty/cache'; + /** + * @var string the directory or path alias pointing to where Smarty cache will be stored. + */ + public $cachePath = '@runtime/Smarty/cache'; - /** - * @var string the directory or path alias pointing to where Smarty compiled templates will be stored. - */ - public $compilePath = '@runtime/Smarty/compile'; + /** + * @var string the directory or path alias pointing to where Smarty compiled templates will be stored. + */ + public $compilePath = '@runtime/Smarty/compile'; - /** - * @var Smarty - */ - public $smarty; + /** + * @var Smarty + */ + public $smarty; - public function init() - { - $this->smarty = new Smarty(); - $this->smarty->setCompileDir(Yii::getAlias($this->compilePath)); - $this->smarty->setCacheDir(Yii::getAlias($this->cachePath)); + public function init() + { + $this->smarty = new Smarty(); + $this->smarty->setCompileDir(Yii::getAlias($this->compilePath)); + $this->smarty->setCacheDir(Yii::getAlias($this->cachePath)); - $this->smarty->registerPlugin('function', 'path', [$this, 'smarty_function_path']); - } + $this->smarty->registerPlugin('function', 'path', [$this, 'smarty_function_path']); + } - /** - * Smarty template function to get a path for using in links - * - * Usage is the following: - * - * {path route='blog/view' alias=$post.alias user=$user.id} - * - * where route is Yii route and the rest of parameters are passed as is. - * - * @param $params - * @param \Smarty_Internal_Template $template - * - * @return string - */ - public function smarty_function_path($params, \Smarty_Internal_Template $template) - { - if (!isset($params['route'])) { - trigger_error("path: missing 'route' parameter"); - } + /** + * Smarty template function to get a path for using in links + * + * Usage is the following: + * + * {path route='blog/view' alias=$post.alias user=$user.id} + * + * where route is Yii route and the rest of parameters are passed as is. + * + * @param $params + * @param \Smarty_Internal_Template $template + * + * @return string + */ + public function smarty_function_path($params, \Smarty_Internal_Template $template) + { + if (!isset($params['route'])) { + trigger_error("path: missing 'route' parameter"); + } - array_unshift($params, $params['route']) ; - unset($params['route']); + array_unshift($params, $params['route']) ; + unset($params['route']); - return Url::to($params); - } + return Url::to($params); + } - /** - * Renders a view file. - * - * This method is invoked by [[View]] whenever it tries to render a view. - * Child classes must implement this method to render the given view file. - * - * @param View $view the view object used for rendering the file. - * @param string $file the view file. - * @param array $params the parameters to be passed to the view file. - * - * @return string the rendering result - */ - public function render($view, $file, $params) - { - /** @var \Smarty_Internal_Template $template */ - $template = $this->smarty->createTemplate($file, null, null, empty($params) ? null : $params, true); + /** + * Renders a view file. + * + * This method is invoked by [[View]] whenever it tries to render a view. + * Child classes must implement this method to render the given view file. + * + * @param View $view the view object used for rendering the file. + * @param string $file the view file. + * @param array $params the parameters to be passed to the view file. + * + * @return string the rendering result + */ + public function render($view, $file, $params) + { + /** @var \Smarty_Internal_Template $template */ + $template = $this->smarty->createTemplate($file, null, null, empty($params) ? null : $params, true); - $template->assign('app', \Yii::$app); - $template->assign('this', $view); + $template->assign('app', \Yii::$app); + $template->assign('this', $view); - return $template->fetch(); - } + return $template->fetch(); + } } diff --git a/extensions/sphinx/ActiveQuery.php b/extensions/sphinx/ActiveQuery.php index 42391f97ae4..31e86c989be 100644 --- a/extensions/sphinx/ActiveQuery.php +++ b/extensions/sphinx/ActiveQuery.php @@ -82,186 +82,193 @@ */ class ActiveQuery extends Query implements ActiveQueryInterface { - use ActiveQueryTrait; - use ActiveRelationTrait; + use ActiveQueryTrait; + use ActiveRelationTrait; - /** - * @var string the SQL statement to be executed for retrieving AR records. - * This is set by [[ActiveRecord::findBySql()]]. - */ - public $sql; + /** + * @var string the SQL statement to be executed for retrieving AR records. + * This is set by [[ActiveRecord::findBySql()]]. + */ + public $sql; - /** - * Sets the [[snippetCallback]] to [[fetchSnippetSourceFromModels()]], which allows to - * fetch the snippet source strings from the Active Record models, using method - * [[ActiveRecord::getSnippetSource()]]. - * For example: - * - * ~~~ - * class Article extends ActiveRecord - * { - * public function getSnippetSource() - * { - * return file_get_contents('/path/to/source/files/' . $this->id . '.txt');; - * } - * } - * - * $articles = Article::find()->snippetByModel()->all(); - * ~~~ - * - * Warning: this option should NOT be used with [[asArray]] at the same time! - * @return static the query object itself - */ - public function snippetByModel() - { - $this->snippetCallback([$this, 'fetchSnippetSourceFromModels']); - return $this; - } + /** + * Sets the [[snippetCallback]] to [[fetchSnippetSourceFromModels()]], which allows to + * fetch the snippet source strings from the Active Record models, using method + * [[ActiveRecord::getSnippetSource()]]. + * For example: + * + * ~~~ + * class Article extends ActiveRecord + * { + * public function getSnippetSource() + * { + * return file_get_contents('/path/to/source/files/' . $this->id . '.txt');; + * } + * } + * + * $articles = Article::find()->snippetByModel()->all(); + * ~~~ + * + * Warning: this option should NOT be used with [[asArray]] at the same time! + * @return static the query object itself + */ + public function snippetByModel() + { + $this->snippetCallback([$this, 'fetchSnippetSourceFromModels']); - /** - * Executes query and returns all results as an array. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $command = $this->createCommand($db); - $rows = $command->queryAll(); - if (!empty($rows)) { - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - $models = $this->fillUpSnippets($models); - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - return $models; - } else { - return []; - } - } + return $this; + } - /** - * Executes query and returns a single row of result. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - $command = $this->createCommand($db); - $row = $command->queryOne(); - if ($row !== false) { - if ($this->asArray) { - $model = $row; - } else { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - list ($model) = $this->fillUpSnippets([$model]); - if (!$this->asArray) { - $model->afterFind(); - } - return $model; - } else { - return null; - } - } + /** + * Executes query and returns all results as an array. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $command = $this->createCommand($db); + $rows = $command->queryAll(); + if (!empty($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + $models = $this->fillUpSnippets($models); + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($this->primaryModel !== null) { - // lazy loading a relational query - if ($this->via instanceof self) { - // via pivot index - $viaModels = $this->via->findPivotRows([$this->primaryModel]); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveQuery $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - } + return $models; + } else { + return []; + } + } - $this->setConnection($db); - $db = $this->getConnection(); + /** + * Executes query and returns a single row of result. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + $command = $this->createCommand($db); + $row = $command->queryOne(); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + list ($model) = $this->fillUpSnippets([$model]); + if (!$this->asArray) { + $model->afterFind(); + } - $params = $this->params; - if ($this->sql === null) { - list ($this->sql, $params) = $db->getQueryBuilder()->build($this); - } - return $db->createCommand($this->sql, $params); - } + return $model; + } else { + return null; + } + } - /** - * @inheritdoc - */ - protected function defaultConnection() - { - $modelClass = $this->modelClass; - return $modelClass::getDb(); - } + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + if ($this->primaryModel !== null) { + // lazy loading a relational query + if ($this->via instanceof self) { + // via pivot index + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveQuery $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); + } else { + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; + } + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); + } + } - /** - * Fetches the source for the snippets using [[ActiveRecord::getSnippetSource()]] method. - * @param ActiveRecord[] $models raw query result rows. - * @throws \yii\base\InvalidCallException if [[asArray]] enabled. - * @return array snippet source strings - */ - protected function fetchSnippetSourceFromModels($models) - { - if ($this->asArray) { - throw new InvalidCallException('"' . __METHOD__ . '" unable to determine snippet source from plain array. Either disable "asArray" option or use regular "snippetCallback"'); - } - $result = []; - foreach ($models as $model) { - $result[] = $model->getSnippetSource(); - } - return $result; - } + $this->setConnection($db); + $db = $this->getConnection(); - /** - * @inheritdoc - */ - protected function callSnippets(array $source) - { - $from = $this->from; - if ($from === null) { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - $tableName = $modelClass::indexName(); - $from = [$tableName]; - } - return $this->callSnippetsInternal($source, $from[0]); - } + $params = $this->params; + if ($this->sql === null) { + list ($this->sql, $params) = $db->getQueryBuilder()->build($this); + } + + return $db->createCommand($this->sql, $params); + } + + /** + * @inheritdoc + */ + protected function defaultConnection() + { + $modelClass = $this->modelClass; + + return $modelClass::getDb(); + } + + /** + * Fetches the source for the snippets using [[ActiveRecord::getSnippetSource()]] method. + * @param ActiveRecord[] $models raw query result rows. + * @throws \yii\base\InvalidCallException if [[asArray]] enabled. + * @return array snippet source strings + */ + protected function fetchSnippetSourceFromModels($models) + { + if ($this->asArray) { + throw new InvalidCallException('"' . __METHOD__ . '" unable to determine snippet source from plain array. Either disable "asArray" option or use regular "snippetCallback"'); + } + $result = []; + foreach ($models as $model) { + $result[] = $model->getSnippetSource(); + } + + return $result; + } + + /** + * @inheritdoc + */ + protected function callSnippets(array $source) + { + $from = $this->from; + if ($from === null) { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + $tableName = $modelClass::indexName(); + $from = [$tableName]; + } + + return $this->callSnippetsInternal($source, $from[0]); + } } diff --git a/extensions/sphinx/ActiveRecord.php b/extensions/sphinx/ActiveRecord.php index cbfe9e1e629..e25ab70f34e 100644 --- a/extensions/sphinx/ActiveRecord.php +++ b/extensions/sphinx/ActiveRecord.php @@ -29,622 +29,636 @@ */ abstract class ActiveRecord extends BaseActiveRecord { - /** - * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. - */ - const OP_INSERT = 0x01; - /** - * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. - */ - const OP_UPDATE = 0x02; - /** - * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. - */ - const OP_DELETE = 0x04; - /** - * All three operations: insert, update, delete. - * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE. - */ - const OP_ALL = 0x07; - - /** - * @var string current snippet value for this Active Record instance. - * It will be filled up automatically when instance found using [[Query::snippetCallback]] - * or [[ActiveQuery::snippetByModel()]]. - */ - private $_snippet; - - /** - * Returns the Sphinx connection used by this AR class. - * By default, the "sphinx" application component is used as the Sphinx connection. - * You may override this method if you want to use a different Sphinx connection. - * @return Connection the Sphinx connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->getComponent('sphinx'); - } - - /** - * Creates an [[ActiveQuery]] instance with a given SQL statement. - * - * Note that because the SQL statement is already specified, calling additional - * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]] - * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is - * still fine. - * - * Below is an example: - * - * ~~~ - * $customers = Article::findBySql("SELECT * FROM `idx_article` WHERE MATCH('development')")->all(); - * ~~~ - * - * @param string $sql the SQL statement to be executed - * @param array $params parameters to be bound to the SQL statement during execution. - * @return ActiveQuery the newly created [[ActiveQuery]] instance - */ - public static function findBySql($sql, $params = []) - { - $query = static::createQuery(); - $query->sql = $sql; - return $query->params($params); - } - - /** - * Updates the whole table using the provided attribute values and conditions. - * For example, to change the status to be 1 for all articles which status is 2: - * - * ~~~ - * Article::updateAll(['status' => 1], 'status = 2'); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = '', $params = []) - { - $command = static::getDb()->createCommand(); - $command->update(static::indexName(), $attributes, $condition, $params); - return $command->execute(); - } - - /** - * Deletes rows in the index using the provided conditions. - * - * For example, to delete all articles whose status is 3: - * - * ~~~ - * Article::deleteAll('status = 3'); - * ~~~ - * - * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = '', $params = []) - { - $command = static::getDb()->createCommand(); - $command->delete(static::indexName(), $condition, $params); - return $command->execute(); - } - - /** - * Creates an [[ActiveQuery]] instance. - * - * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also - * by [[hasOne()]] and [[hasMany()]] to create a relational query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ActiveQuery($config); - } - - /** - * Declares the name of the Sphinx index associated with this AR class. - * By default this method returns the class name as the index name by calling [[Inflector::camel2id()]]. - * For example, 'Article' becomes 'article', and 'StockItem' becomes - * 'stock_item'. You may override this method if the index is not named after this convention. - * @return string the index name - */ - public static function indexName() - { - return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); - } - - /** - * Returns the schema information of the Sphinx index associated with this AR class. - * @return IndexSchema the schema information of the Sphinx index associated with this AR class. - * @throws InvalidConfigException if the index for the AR class does not exist. - */ - public static function getIndexSchema() - { - $schema = static::getDb()->getIndexSchema(static::indexName()); - if ($schema !== null) { - return $schema; - } else { - throw new InvalidConfigException("The index does not exist: " . static::indexName()); - } - } - - /** - * Returns the primary key name for this AR class. - * The default implementation will return the primary key as declared - * in the Sphinx index, which is associated with this AR class. - * - * Note that an array should be returned even for a table with single primary key. - * - * @return string[] the primary keys of the associated Sphinx index. - */ - public static function primaryKey() - { - return [static::getIndexSchema()->primaryKey]; - } - - /** - * Builds a snippet from provided data and query, using specified index settings. - * @param string|array $source is the source data to extract a snippet from. - * It could be either a single string or array of strings. - * @param string $match the full-text query to build snippets for. - * @param array $options list of options in format: optionName => optionValue - * @return string|array built snippet in case "source" is a string, list of built snippets - * in case "source" is an array. - */ - public static function callSnippets($source, $match, $options = []) - { - $command = static::getDb()->createCommand(); - $command->callSnippets(static::indexName(), $source, $match, $options); - if (is_array($source)) { - return $command->queryColumn(); - } else { - return $command->queryScalar(); - } - } - - /** - * Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics. - * @param string $text the text to break down to keywords. - * @param boolean $fetchStatistic whether to return document and hit occurrence statistics - * @return array keywords and statistics - */ - public static function callKeywords($text, $fetchStatistic = false) - { - $command = static::getDb()->createCommand(); - $command->callKeywords(static::indexName(), $text, $fetchStatistic); - return $command->queryAll(); - } - - /** - * @param string $snippet - */ - public function setSnippet($snippet) - { - $this->_snippet = $snippet; - } - - /** - * Returns current snippet value or generates new one from given match. - * @param string $match snippet source query - * @param array $options list of options in format: optionName => optionValue - * @return string snippet value - */ - public function getSnippet($match = null, $options = []) - { - if ($match !== null) { - $this->_snippet = $this->fetchSnippet($match, $options); - } - return $this->_snippet; - } - - /** - * Builds up the snippet value from the given query. - * @param string $match the full-text query to build snippets for. - * @param array $options list of options in format: optionName => optionValue - * @return string snippet value. - */ - protected function fetchSnippet($match, $options = []) - { - return static::callSnippets($this->getSnippetSource(), $match, $options); - } - - /** - * Returns the string, which should be used as a source to create snippet for this - * Active Record instance. - * Child classes must implement this method to return the actual snippet source text. - * For example: - * ~~~ - * public function getSnippetSource() - * { - * return $this->snippetSourceRelation->content; - * } - * ~~~ - * @return string snippet source string. - * @throws \yii\base\NotSupportedException if this is not supported by the Active Record class - */ - public function getSnippetSource() - { - throw new NotSupportedException($this->className() . ' does not provide snippet source.'); - } - - /** - * Declares which operations should be performed within a transaction in different scenarios. - * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]], - * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively. - * By default, these methods are NOT enclosed in a transaction. - * - * In some scenarios, to ensure data consistency, you may want to enclose some or all of them - * in transactions. You can do so by overriding this method and returning the operations - * that need to be transactional. For example, - * - * ~~~ - * return [ - * 'admin' => self::OP_INSERT, - * 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, - * // the above is equivalent to the following: - * // 'api' => self::OP_ALL, - * - * ]; - * ~~~ - * - * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]]) - * should be done in a transaction; and in the "api" scenario, all the operations should be done - * in a transaction. - * - * @return array the declarations of transactional operations. The array keys are scenarios names, - * and the array values are the corresponding transaction operations. - */ - public function transactions() - { - return []; - } - - /** - * Returns the list of all attribute names of the model. - * The default implementation will return all column names of the table associated with this AR class. - * @return array list of attribute names. - */ - public function attributes() - { - return array_keys(static::getIndexSchema()->columns); - } - - /** - * Inserts a row into the associated Sphinx index using the attribute values of this record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. insert the record into index. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[changedAttributes|changed attribute values]] will be inserted. - * - * For example, to insert an article record: - * - * ~~~ - * $article = new Article; - * $article->id = $id; - * $article->genre_id = $genreId; - * $article->content = $content; - * $article->insert(); - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from index will be saved. - * @return boolean whether the attributes are valid and the record is inserted successfully. - * @throws \Exception in case insert failed. - */ - public function insert($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - $db = static::getDb(); - if ($this->isTransactional(self::OP_INSERT) && $db->getTransaction() === null) { - $transaction = $db->beginTransaction(); - try { - $result = $this->insertInternal($attributes); - if ($result === false) { - $transaction->rollBack(); - } else { - $transaction->commit(); - } - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } else { - $result = $this->insertInternal($attributes); - } - return $result; - } - - /** - * @see ActiveRecord::insert() - */ - private function insertInternal($attributes = null) - { - if (!$this->beforeSave(true)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - foreach ($this->getPrimaryKey(true) as $key => $value) { - $values[$key] = $value; - } - } - $db = static::getDb(); - $command = $db->createCommand()->insert($this->indexName(), $values); - if (!$command->execute()) { - return false; - } - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $value); - } - $this->afterSave(true); - return true; - } - - /** - * Saves the changes to this active record into the associated Sphinx index. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. save the record into index. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[changedAttributes|changed attribute values]] will be saved into database. - * - * For example, to update an article record: - * - * ~~~ - * $article = Article::find(['id' => $id]); - * $article->genre_id = $genreId; - * $article->group_id = $groupId; - * $article->update(); - * ~~~ - * - * Note that it is possible the update does not affect any row in the table. - * In this case, this method will return 0. For this reason, you should use the following - * code to check if update() is successful or not: - * - * ~~~ - * if ($this->update() !== false) { - * // update successful - * } else { - * // update failed - * } - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return integer|boolean the number of rows affected, or false if validation fails - * or [[beforeSave()]] stops the updating process. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being updated is outdated. - * @throws \Exception in case update failed. - */ - public function update($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - $db = static::getDb(); - if ($this->isTransactional(self::OP_UPDATE) && $db->getTransaction() === null) { - $transaction = $db->beginTransaction(); - try { - $result = $this->updateInternal($attributes); - if ($result === false) { - $transaction->rollBack(); - } else { - $transaction->commit(); - } - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } else { - $result = $this->updateInternal($attributes); - } - return $result; - } - - /** - * @see CActiveRecord::update() - * @throws StaleObjectException - */ - protected function updateInternal($attributes = null) - { - if (!$this->beforeSave(false)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $this->afterSave(false); - return 0; - } - - // Replace is supported only by runtime indexes and necessary only for field update - $useReplace = false; - $indexSchema = $this->getIndexSchema(); - if ($this->getIndexSchema()->isRuntime) { - foreach ($values as $name => $value) { - $columnSchema = $indexSchema->getColumn($name); - if ($columnSchema->isField) { - $useReplace = true; - break; - } - } - } - - if ($useReplace) { - $values = array_merge($values, $this->getOldPrimaryKey(true)); - $command = static::getDb()->createCommand(); - $command->replace(static::indexName(), $values); - // We do not check the return value of replace because it's possible - // that the REPLACE statement doesn't change anything and thus returns 0. - $rows = $command->execute(); - } else { - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - if (!isset($values[$lock])) { - $values[$lock] = $this->$lock + 1; - } - $condition[$lock] = $this->$lock; - } - // We do not check the return value of updateAll() because it's possible - // that the UPDATE statement doesn't change anything and thus returns 0. - $rows = $this->updateAll($values, $condition); - - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); - } - } - - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $this->getAttribute($name)); - } - $this->afterSave(false); - return $rows; - } - - /** - * Deletes the index entry corresponding to this active record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeDelete()]]. If the method returns false, it will skip the - * rest of the steps; - * 2. delete the record from the index; - * 3. call [[afterDelete()]]. - * - * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] - * will be raised by the corresponding methods. - * - * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being deleted is outdated. - * @throws \Exception in case delete failed. - */ - public function delete() - { - $db = static::getDb(); - $transaction = $this->isTransactional(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null; - try { - $result = false; - if ($this->beforeDelete()) { - // we do not check the return value of deleteAll() because it's possible - // the record is already deleted in the database and thus the method will return 0 - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - $condition[$lock] = $this->$lock; - } - $result = $this->deleteAll($condition); - if ($lock !== null && !$result) { - throw new StaleObjectException('The object being deleted is outdated.'); - } - $this->setOldAttributes(null); - $this->afterDelete(); - } - if ($transaction !== null) { - if ($result === false) { - $transaction->rollBack(); - } else { - $transaction->commit(); - } - } - } catch (\Exception $e) { - if ($transaction !== null) { - $transaction->rollBack(); - } - throw $e; - } - return $result; - } - - /** - * Returns a value indicating whether the given active record is the same as the current one. - * The comparison is made by comparing the index names and the primary key values of the two active records. - * If one of the records [[isNewRecord|is new]] they are also considered not equal. - * @param ActiveRecord $record record to compare to - * @return boolean whether the two active records refer to the same row in the same index. - */ - public function equals($record) - { - if ($this->isNewRecord || $record->isNewRecord) { - return false; - } - return $this->indexName() === $record->indexName() && $this->getPrimaryKey() === $record->getPrimaryKey(); - } - - /** - * @inheritdoc - */ - public static function populateRecord($record, $row) - { - $columns = static::getIndexSchema()->columns; - foreach ($row as $name => $value) { - if (isset($columns[$name]) && $columns[$name]->isMva) { - $row[$name] = explode(',', $value); - } - } - parent::populateRecord($record, $row); - } - - /** - * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. - * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. - * @return boolean whether the specified operation is transactional in the current [[scenario]]. - */ - public function isTransactional($operation) - { - $scenario = $this->getScenario(); - $transactions = $this->transactions(); - return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); - } + /** + * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_INSERT = 0x01; + /** + * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_UPDATE = 0x02; + /** + * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_DELETE = 0x04; + /** + * All three operations: insert, update, delete. + * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE. + */ + const OP_ALL = 0x07; + + /** + * @var string current snippet value for this Active Record instance. + * It will be filled up automatically when instance found using [[Query::snippetCallback]] + * or [[ActiveQuery::snippetByModel()]]. + */ + private $_snippet; + + /** + * Returns the Sphinx connection used by this AR class. + * By default, the "sphinx" application component is used as the Sphinx connection. + * You may override this method if you want to use a different Sphinx connection. + * @return Connection the Sphinx connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getComponent('sphinx'); + } + + /** + * Creates an [[ActiveQuery]] instance with a given SQL statement. + * + * Note that because the SQL statement is already specified, calling additional + * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]] + * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is + * still fine. + * + * Below is an example: + * + * ~~~ + * $customers = Article::findBySql("SELECT * FROM `idx_article` WHERE MATCH('development')")->all(); + * ~~~ + * + * @param string $sql the SQL statement to be executed + * @param array $params parameters to be bound to the SQL statement during execution. + * @return ActiveQuery the newly created [[ActiveQuery]] instance + */ + public static function findBySql($sql, $params = []) + { + $query = static::createQuery(); + $query->sql = $sql; + + return $query->params($params); + } + + /** + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all articles which status is 2: + * + * ~~~ + * Article::updateAll(['status' => 1], 'status = 2'); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = '', $params = []) + { + $command = static::getDb()->createCommand(); + $command->update(static::indexName(), $attributes, $condition, $params); + + return $command->execute(); + } + + /** + * Deletes rows in the index using the provided conditions. + * + * For example, to delete all articles whose status is 3: + * + * ~~~ + * Article::deleteAll('status = 3'); + * ~~~ + * + * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = '', $params = []) + { + $command = static::getDb()->createCommand(); + $command->delete(static::indexName(), $condition, $params); + + return $command->execute(); + } + + /** + * Creates an [[ActiveQuery]] instance. + * + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also + * by [[hasOne()]] and [[hasMany()]] to create a relational query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new ActiveQuery($config); + } + + /** + * Declares the name of the Sphinx index associated with this AR class. + * By default this method returns the class name as the index name by calling [[Inflector::camel2id()]]. + * For example, 'Article' becomes 'article', and 'StockItem' becomes + * 'stock_item'. You may override this method if the index is not named after this convention. + * @return string the index name + */ + public static function indexName() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); + } + + /** + * Returns the schema information of the Sphinx index associated with this AR class. + * @return IndexSchema the schema information of the Sphinx index associated with this AR class. + * @throws InvalidConfigException if the index for the AR class does not exist. + */ + public static function getIndexSchema() + { + $schema = static::getDb()->getIndexSchema(static::indexName()); + if ($schema !== null) { + return $schema; + } else { + throw new InvalidConfigException("The index does not exist: " . static::indexName()); + } + } + + /** + * Returns the primary key name for this AR class. + * The default implementation will return the primary key as declared + * in the Sphinx index, which is associated with this AR class. + * + * Note that an array should be returned even for a table with single primary key. + * + * @return string[] the primary keys of the associated Sphinx index. + */ + public static function primaryKey() + { + return [static::getIndexSchema()->primaryKey]; + } + + /** + * Builds a snippet from provided data and query, using specified index settings. + * @param string|array $source is the source data to extract a snippet from. + * It could be either a single string or array of strings. + * @param string $match the full-text query to build snippets for. + * @param array $options list of options in format: optionName => optionValue + * @return string|array built snippet in case "source" is a string, list of built snippets + * in case "source" is an array. + */ + public static function callSnippets($source, $match, $options = []) + { + $command = static::getDb()->createCommand(); + $command->callSnippets(static::indexName(), $source, $match, $options); + if (is_array($source)) { + return $command->queryColumn(); + } else { + return $command->queryScalar(); + } + } + + /** + * Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics. + * @param string $text the text to break down to keywords. + * @param boolean $fetchStatistic whether to return document and hit occurrence statistics + * @return array keywords and statistics + */ + public static function callKeywords($text, $fetchStatistic = false) + { + $command = static::getDb()->createCommand(); + $command->callKeywords(static::indexName(), $text, $fetchStatistic); + + return $command->queryAll(); + } + + /** + * @param string $snippet + */ + public function setSnippet($snippet) + { + $this->_snippet = $snippet; + } + + /** + * Returns current snippet value or generates new one from given match. + * @param string $match snippet source query + * @param array $options list of options in format: optionName => optionValue + * @return string snippet value + */ + public function getSnippet($match = null, $options = []) + { + if ($match !== null) { + $this->_snippet = $this->fetchSnippet($match, $options); + } + + return $this->_snippet; + } + + /** + * Builds up the snippet value from the given query. + * @param string $match the full-text query to build snippets for. + * @param array $options list of options in format: optionName => optionValue + * @return string snippet value. + */ + protected function fetchSnippet($match, $options = []) + { + return static::callSnippets($this->getSnippetSource(), $match, $options); + } + + /** + * Returns the string, which should be used as a source to create snippet for this + * Active Record instance. + * Child classes must implement this method to return the actual snippet source text. + * For example: + * ~~~ + * public function getSnippetSource() + * { + * return $this->snippetSourceRelation->content; + * } + * ~~~ + * @return string snippet source string. + * @throws \yii\base\NotSupportedException if this is not supported by the Active Record class + */ + public function getSnippetSource() + { + throw new NotSupportedException($this->className() . ' does not provide snippet source.'); + } + + /** + * Declares which operations should be performed within a transaction in different scenarios. + * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]], + * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively. + * By default, these methods are NOT enclosed in a transaction. + * + * In some scenarios, to ensure data consistency, you may want to enclose some or all of them + * in transactions. You can do so by overriding this method and returning the operations + * that need to be transactional. For example, + * + * ~~~ + * return [ + * 'admin' => self::OP_INSERT, + * 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, + * // the above is equivalent to the following: + * // 'api' => self::OP_ALL, + * + * ]; + * ~~~ + * + * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]]) + * should be done in a transaction; and in the "api" scenario, all the operations should be done + * in a transaction. + * + * @return array the declarations of transactional operations. The array keys are scenarios names, + * and the array values are the corresponding transaction operations. + */ + public function transactions() + { + return []; + } + + /** + * Returns the list of all attribute names of the model. + * The default implementation will return all column names of the table associated with this AR class. + * @return array list of attribute names. + */ + public function attributes() + { + return array_keys(static::getIndexSchema()->columns); + } + + /** + * Inserts a row into the associated Sphinx index using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into index. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[changedAttributes|changed attribute values]] will be inserted. + * + * For example, to insert an article record: + * + * ~~~ + * $article = new Article; + * $article->id = $id; + * $article->genre_id = $genreId; + * $article->content = $content; + * $article->insert(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from index will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws \Exception in case insert failed. + */ + public function insert($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $db = static::getDb(); + if ($this->isTransactional(self::OP_INSERT) && $db->getTransaction() === null) { + $transaction = $db->beginTransaction(); + try { + $result = $this->insertInternal($attributes); + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + } catch (\Exception $e) { + $transaction->rollBack(); + throw $e; + } + } else { + $result = $this->insertInternal($attributes); + } + + return $result; + } + + /** + * @see ActiveRecord::insert() + */ + private function insertInternal($attributes = null) + { + if (!$this->beforeSave(true)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + foreach ($this->getPrimaryKey(true) as $key => $value) { + $values[$key] = $value; + } + } + $db = static::getDb(); + $command = $db->createCommand()->insert($this->indexName(), $values); + if (!$command->execute()) { + return false; + } + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $value); + } + $this->afterSave(true); + + return true; + } + + /** + * Saves the changes to this active record into the associated Sphinx index. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. save the record into index. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[changedAttributes|changed attribute values]] will be saved into database. + * + * For example, to update an article record: + * + * ~~~ + * $article = Article::find(['id' => $id]); + * $article->genre_id = $genreId; + * $article->group_id = $groupId; + * $article->update(); + * ~~~ + * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being updated is outdated. + * @throws \Exception in case update failed. + */ + public function update($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $db = static::getDb(); + if ($this->isTransactional(self::OP_UPDATE) && $db->getTransaction() === null) { + $transaction = $db->beginTransaction(); + try { + $result = $this->updateInternal($attributes); + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + } catch (\Exception $e) { + $transaction->rollBack(); + throw $e; + } + } else { + $result = $this->updateInternal($attributes); + } + + return $result; + } + + /** + * @see CActiveRecord::update() + * @throws StaleObjectException + */ + protected function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false); + + return 0; + } + + // Replace is supported only by runtime indexes and necessary only for field update + $useReplace = false; + $indexSchema = $this->getIndexSchema(); + if ($this->getIndexSchema()->isRuntime) { + foreach ($values as $name => $value) { + $columnSchema = $indexSchema->getColumn($name); + if ($columnSchema->isField) { + $useReplace = true; + break; + } + } + } + + if ($useReplace) { + $values = array_merge($values, $this->getOldPrimaryKey(true)); + $command = static::getDb()->createCommand(); + $command->replace(static::indexName(), $values); + // We do not check the return value of replace because it's possible + // that the REPLACE statement doesn't change anything and thus returns 0. + $rows = $command->execute(); + } else { + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } + // We do not check the return value of updateAll() because it's possible + // that the UPDATE statement doesn't change anything and thus returns 0. + $rows = $this->updateAll($values, $condition); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + } + + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $this->getAttribute($name)); + } + $this->afterSave(false); + + return $rows; + } + + /** + * Deletes the index entry corresponding to this active record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeDelete()]]. If the method returns false, it will skip the + * rest of the steps; + * 2. delete the record from the index; + * 3. call [[afterDelete()]]. + * + * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] + * will be raised by the corresponding methods. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. + * @throws \Exception in case delete failed. + */ + public function delete() + { + $db = static::getDb(); + $transaction = $this->isTransactional(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null; + try { + $result = false; + if ($this->beforeDelete()) { + // we do not check the return value of deleteAll() because it's possible + // the record is already deleted in the database and thus the method will return 0 + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = $this->deleteAll($condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->setOldAttributes(null); + $this->afterDelete(); + } + if ($transaction !== null) { + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + } + } catch (\Exception $e) { + if ($transaction !== null) { + $transaction->rollBack(); + } + throw $e; + } + + return $result; + } + + /** + * Returns a value indicating whether the given active record is the same as the current one. + * The comparison is made by comparing the index names and the primary key values of the two active records. + * If one of the records [[isNewRecord|is new]] they are also considered not equal. + * @param ActiveRecord $record record to compare to + * @return boolean whether the two active records refer to the same row in the same index. + */ + public function equals($record) + { + if ($this->isNewRecord || $record->isNewRecord) { + return false; + } + + return $this->indexName() === $record->indexName() && $this->getPrimaryKey() === $record->getPrimaryKey(); + } + + /** + * @inheritdoc + */ + public static function populateRecord($record, $row) + { + $columns = static::getIndexSchema()->columns; + foreach ($row as $name => $value) { + if (isset($columns[$name]) && $columns[$name]->isMva) { + $row[$name] = explode(',', $value); + } + } + parent::populateRecord($record, $row); + } + + /** + * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. + * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. + * @return boolean whether the specified operation is transactional in the current [[scenario]]. + */ + public function isTransactional($operation) + { + $scenario = $this->getScenario(); + $transactions = $this->transactions(); + + return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); + } } diff --git a/extensions/sphinx/ColumnSchema.php b/extensions/sphinx/ColumnSchema.php index a7736f772fe..76c66e11934 100644 --- a/extensions/sphinx/ColumnSchema.php +++ b/extensions/sphinx/ColumnSchema.php @@ -18,64 +18,65 @@ */ class ColumnSchema extends Object { - /** - * @var string name of this column (without quotes). - */ - public $name; - /** - * @var string abstract type of this column. Possible abstract types include: - * string, text, boolean, smallint, integer, bigint, float, decimal, datetime, - * timestamp, time, date, binary, and money. - */ - public $type; - /** - * @var string the PHP type of this column. Possible PHP types include: - * string, boolean, integer, double. - */ - public $phpType; - /** - * @var string the DB type of this column. Possible DB types vary according to the type of DBMS. - */ - public $dbType; - /** - * @var boolean whether this column is a primary key - */ - public $isPrimaryKey; - /** - * @var boolean whether this column is an attribute - */ - public $isAttribute; - /** - * @var boolean whether this column is a indexed field - */ - public $isField; - /** - * @var boolean whether this column is a multi value attribute (MVA) - */ - public $isMva; + /** + * @var string name of this column (without quotes). + */ + public $name; + /** + * @var string abstract type of this column. Possible abstract types include: + * string, text, boolean, smallint, integer, bigint, float, decimal, datetime, + * timestamp, time, date, binary, and money. + */ + public $type; + /** + * @var string the PHP type of this column. Possible PHP types include: + * string, boolean, integer, double. + */ + public $phpType; + /** + * @var string the DB type of this column. Possible DB types vary according to the type of DBMS. + */ + public $dbType; + /** + * @var boolean whether this column is a primary key + */ + public $isPrimaryKey; + /** + * @var boolean whether this column is an attribute + */ + public $isAttribute; + /** + * @var boolean whether this column is a indexed field + */ + public $isField; + /** + * @var boolean whether this column is a multi value attribute (MVA) + */ + public $isMva; - /** - * Converts the input value according to [[phpType]]. - * If the value is null or an [[Expression]], it will not be converted. - * @param mixed $value input value - * @return mixed converted value - */ - public function typecast($value) - { - if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { - return $value; - } - if ($value === '' && $this->type !== Schema::TYPE_STRING) { - return null; - } - switch ($this->phpType) { - case 'string': - return (string)$value; - case 'integer': - return (integer)$value; - case 'boolean': - return (boolean)$value; - } - return $value; - } + /** + * Converts the input value according to [[phpType]]. + * If the value is null or an [[Expression]], it will not be converted. + * @param mixed $value input value + * @return mixed converted value + */ + public function typecast($value) + { + if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { + return $value; + } + if ($value === '' && $this->type !== Schema::TYPE_STRING) { + return null; + } + switch ($this->phpType) { + case 'string': + return (string) $value; + case 'integer': + return (integer) $value; + case 'boolean': + return (boolean) $value; + } + + return $value; + } } diff --git a/extensions/sphinx/Command.php b/extensions/sphinx/Command.php index 9197b67cda9..ae0586d5234 100644 --- a/extensions/sphinx/Command.php +++ b/extensions/sphinx/Command.php @@ -44,283 +44,290 @@ */ class Command extends \yii\db\Command { - /** - * @var \yii\sphinx\Connection the Sphinx connection that this command is associated with. - */ - public $db; - - /** - * Creates a batch INSERT command. - * For example, - * - * ~~~ - * $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ])->execute(); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $index the index that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the index - * @return static the command object itself - */ - public function batchInsert($index, $columns, $rows) - { - $params = []; - $sql = $this->db->getQueryBuilder()->batchInsert($index, $columns, $rows, $params); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Creates an REPLACE command. - * For example, - * - * ~~~ - * $connection->createCommand()->insert('idx_user', [ - * 'name' => 'Sam', - * 'age' => 30, - * ])->execute(); - * ~~~ - * - * The method will properly escape the column names, and bind the values to be replaced. - * - * Note that the created command is not executed until [[execute()]] is called. - * - * @param string $index the index that new rows will be replaced into. - * @param array $columns the column data (name => value) to be replaced into the index. - * @return static the command object itself - */ - public function replace($index, $columns) - { - $params = []; - $sql = $this->db->getQueryBuilder()->replace($index, $columns, $params); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Creates a batch REPLACE command. - * For example, - * - * ~~~ - * $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ])->execute(); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $index the index that new rows will be replaced. - * @param array $columns the column names - * @param array $rows the rows to be batch replaced in the index - * @return static the command object itself - */ - public function batchReplace($index, $columns, $rows) - { - $params = []; - $sql = $this->db->getQueryBuilder()->batchReplace($index, $columns, $rows, $params); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Creates an UPDATE command. - * For example, - * - * ~~~ - * $connection->createCommand()->update('tbl_user', ['status' => 1], 'age > 30')->execute(); - * ~~~ - * - * The method will properly escape the column names and bind the values to be updated. - * - * Note that the created command is not executed until [[execute()]] is called. - * - * @param string $index the index to be updated. - * @param array $columns the column data (name => value) to be updated. - * @param string|array $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the parameters to be bound to the command - * @param array $options list of options in format: optionName => optionValue - * @return static the command object itself - */ - public function update($index, $columns, $condition = '', $params = [], $options = []) - { - $sql = $this->db->getQueryBuilder()->update($index, $columns, $condition, $params, $options); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Creates a SQL command for truncating a runtime index. - * @param string $index the index to be truncated. The name will be properly quoted by the method. - * @return static the command object itself - */ - public function truncateIndex($index) - { - $sql = $this->db->getQueryBuilder()->truncateIndex($index); - return $this->setSql($sql); - } - - /** - * Builds a snippet from provided data and query, using specified index settings. - * @param string $index name of the index, from which to take the text processing settings. - * @param string|array $source is the source data to extract a snippet from. - * It could be either a single string or array of strings. - * @param string $match the full-text query to build snippets for. - * @param array $options list of options in format: optionName => optionValue - * @return static the command object itself - */ - public function callSnippets($index, $source, $match, $options = []) - { - $params = []; - $sql = $this->db->getQueryBuilder()->callSnippets($index, $source, $match, $options, $params); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics. - * @param string $index the name of the index from which to take the text processing settings - * @param string $text the text to break down to keywords. - * @param boolean $fetchStatistic whether to return document and hit occurrence statistics - * @return string the SQL statement for call keywords. - */ - public function callKeywords($index, $text, $fetchStatistic = false) - { - $params = []; - $sql = $this->db->getQueryBuilder()->callKeywords($index, $text, $fetchStatistic, $params); - return $this->setSql($sql)->bindValues($params); - } - - // Not Supported : - - /** - * @inheritdoc - */ - public function createTable($table, $columns, $options = null) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function renameTable($table, $newName) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function dropTable($table) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function truncateTable($table) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function addColumn($table, $column, $type) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function dropColumn($table, $column) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function renameColumn($table, $oldName, $newName) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function alterColumn($table, $column, $type) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function addPrimaryKey($name, $table, $columns) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function dropPrimaryKey($name, $table) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function dropForeignKey($name, $table) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function createIndex($name, $table, $columns, $unique = false) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function dropIndex($name, $table) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function resetSequence($table, $value = null) - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } - - /** - * @inheritdoc - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } + /** + * @var \yii\sphinx\Connection the Sphinx connection that this command is associated with. + */ + public $db; + + /** + * Creates a batch INSERT command. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ])->execute(); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $index the index that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the index + * @return static the command object itself + */ + public function batchInsert($index, $columns, $rows) + { + $params = []; + $sql = $this->db->getQueryBuilder()->batchInsert($index, $columns, $rows, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates an REPLACE command. + * For example, + * + * ~~~ + * $connection->createCommand()->insert('idx_user', [ + * 'name' => 'Sam', + * 'age' => 30, + * ])->execute(); + * ~~~ + * + * The method will properly escape the column names, and bind the values to be replaced. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $index the index that new rows will be replaced into. + * @param array $columns the column data (name => value) to be replaced into the index. + * @return static the command object itself + */ + public function replace($index, $columns) + { + $params = []; + $sql = $this->db->getQueryBuilder()->replace($index, $columns, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a batch REPLACE command. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ])->execute(); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $index the index that new rows will be replaced. + * @param array $columns the column names + * @param array $rows the rows to be batch replaced in the index + * @return static the command object itself + */ + public function batchReplace($index, $columns, $rows) + { + $params = []; + $sql = $this->db->getQueryBuilder()->batchReplace($index, $columns, $rows, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates an UPDATE command. + * For example, + * + * ~~~ + * $connection->createCommand()->update('tbl_user', ['status' => 1], 'age > 30')->execute(); + * ~~~ + * + * The method will properly escape the column names and bind the values to be updated. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $index the index to be updated. + * @param array $columns the column data (name => value) to be updated. + * @param string|array $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the parameters to be bound to the command + * @param array $options list of options in format: optionName => optionValue + * @return static the command object itself + */ + public function update($index, $columns, $condition = '', $params = [], $options = []) + { + $sql = $this->db->getQueryBuilder()->update($index, $columns, $condition, $params, $options); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a SQL command for truncating a runtime index. + * @param string $index the index to be truncated. The name will be properly quoted by the method. + * @return static the command object itself + */ + public function truncateIndex($index) + { + $sql = $this->db->getQueryBuilder()->truncateIndex($index); + + return $this->setSql($sql); + } + + /** + * Builds a snippet from provided data and query, using specified index settings. + * @param string $index name of the index, from which to take the text processing settings. + * @param string|array $source is the source data to extract a snippet from. + * It could be either a single string or array of strings. + * @param string $match the full-text query to build snippets for. + * @param array $options list of options in format: optionName => optionValue + * @return static the command object itself + */ + public function callSnippets($index, $source, $match, $options = []) + { + $params = []; + $sql = $this->db->getQueryBuilder()->callSnippets($index, $source, $match, $options, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics. + * @param string $index the name of the index from which to take the text processing settings + * @param string $text the text to break down to keywords. + * @param boolean $fetchStatistic whether to return document and hit occurrence statistics + * @return string the SQL statement for call keywords. + */ + public function callKeywords($index, $text, $fetchStatistic = false) + { + $params = []; + $sql = $this->db->getQueryBuilder()->callKeywords($index, $text, $fetchStatistic, $params); + + return $this->setSql($sql)->bindValues($params); + } + + // Not Supported : + + /** + * @inheritdoc + */ + public function createTable($table, $columns, $options = null) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function renameTable($table, $newName) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function dropTable($table) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function truncateTable($table) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function addColumn($table, $column, $type) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function dropColumn($table, $column) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function renameColumn($table, $oldName, $newName) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function alterColumn($table, $column, $type) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function addPrimaryKey($name, $table, $columns) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function dropPrimaryKey($name, $table) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function dropForeignKey($name, $table) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function createIndex($name, $table, $columns, $unique = false) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function dropIndex($name, $table) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function resetSequence($table, $value = null) + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } + + /** + * @inheritdoc + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } } diff --git a/extensions/sphinx/Connection.php b/extensions/sphinx/Connection.php index c9c67ab6649..3548270e20b 100644 --- a/extensions/sphinx/Connection.php +++ b/extensions/sphinx/Connection.php @@ -59,72 +59,73 @@ */ class Connection extends \yii\db\Connection { - /** - * @inheritdoc - */ - public $schemaMap = [ - 'mysqli' => 'yii\sphinx\Schema', // MySQL - 'mysql' => 'yii\sphinx\Schema', // MySQL - ]; + /** + * @inheritdoc + */ + public $schemaMap = [ + 'mysqli' => 'yii\sphinx\Schema', // MySQL + 'mysql' => 'yii\sphinx\Schema', // MySQL + ]; - /** - * Obtains the schema information for the named index. - * @param string $name index name. - * @param boolean $refresh whether to reload the table schema even if it is found in the cache. - * @return IndexSchema index schema information. Null if the named index does not exist. - */ - public function getIndexSchema($name, $refresh = false) - { - return $this->getSchema()->getIndexSchema($name, $refresh); - } + /** + * Obtains the schema information for the named index. + * @param string $name index name. + * @param boolean $refresh whether to reload the table schema even if it is found in the cache. + * @return IndexSchema index schema information. Null if the named index does not exist. + */ + public function getIndexSchema($name, $refresh = false) + { + return $this->getSchema()->getIndexSchema($name, $refresh); + } - /** - * Quotes a index name for use in a query. - * If the index name contains schema prefix, the prefix will also be properly quoted. - * If the index name is already quoted or contains special characters including '(', '[[' and '{{', - * then this method will do nothing. - * @param string $name index name - * @return string the properly quoted index name - */ - public function quoteIndexName($name) - { - return $this->getSchema()->quoteIndexName($name); - } + /** + * Quotes a index name for use in a query. + * If the index name contains schema prefix, the prefix will also be properly quoted. + * If the index name is already quoted or contains special characters including '(', '[[' and '{{', + * then this method will do nothing. + * @param string $name index name + * @return string the properly quoted index name + */ + public function quoteIndexName($name) + { + return $this->getSchema()->quoteIndexName($name); + } - /** - * Alias of [[quoteIndexName()]]. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - return $this->quoteIndexName($name); - } + /** + * Alias of [[quoteIndexName()]]. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return $this->quoteIndexName($name); + } - /** - * Creates a command for execution. - * @param string $sql the SQL statement to be executed - * @param array $params the parameters to be bound to the SQL statement - * @return Command the Sphinx command - */ - public function createCommand($sql = null, $params = []) - { - $this->open(); - $command = new Command([ - 'db' => $this, - 'sql' => $sql, - ]); - return $command->bindValues($params); - } + /** + * Creates a command for execution. + * @param string $sql the SQL statement to be executed + * @param array $params the parameters to be bound to the SQL statement + * @return Command the Sphinx command + */ + public function createCommand($sql = null, $params = []) + { + $this->open(); + $command = new Command([ + 'db' => $this, + 'sql' => $sql, + ]); - /** - * This method is not supported by Sphinx. - * @param string $sequenceName name of the sequence object - * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object - * @throws \yii\base\NotSupportedException always. - */ - public function getLastInsertID($sequenceName = '') - { - throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); - } + return $command->bindValues($params); + } + + /** + * This method is not supported by Sphinx. + * @param string $sequenceName name of the sequence object + * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object + * @throws \yii\base\NotSupportedException always. + */ + public function getLastInsertID($sequenceName = '') + { + throw new NotSupportedException('"' . __METHOD__ . '" is not supported.'); + } } diff --git a/extensions/sphinx/IndexSchema.php b/extensions/sphinx/IndexSchema.php index 4e612195788..073c1e7e5f4 100644 --- a/extensions/sphinx/IndexSchema.php +++ b/extensions/sphinx/IndexSchema.php @@ -19,44 +19,44 @@ */ class IndexSchema extends Object { - /** - * @var string name of this index. - */ - public $name; - /** - * @var string type of the index. - */ - public $type; - /** - * @var boolean whether this index is a runtime index. - */ - public $isRuntime; - /** - * @var string primary key of this index. - */ - public $primaryKey; - /** - * @var ColumnSchema[] column metadata of this index. Each array element is a [[ColumnSchema]] object, indexed by column names. - */ - public $columns = []; + /** + * @var string name of this index. + */ + public $name; + /** + * @var string type of the index. + */ + public $type; + /** + * @var boolean whether this index is a runtime index. + */ + public $isRuntime; + /** + * @var string primary key of this index. + */ + public $primaryKey; + /** + * @var ColumnSchema[] column metadata of this index. Each array element is a [[ColumnSchema]] object, indexed by column names. + */ + public $columns = []; - /** - * Gets the named column metadata. - * This is a convenient method for retrieving a named column even if it does not exist. - * @param string $name column name - * @return ColumnSchema metadata of the named column. Null if the named column does not exist. - */ - public function getColumn($name) - { - return isset($this->columns[$name]) ? $this->columns[$name] : null; - } + /** + * Gets the named column metadata. + * This is a convenient method for retrieving a named column even if it does not exist. + * @param string $name column name + * @return ColumnSchema metadata of the named column. Null if the named column does not exist. + */ + public function getColumn($name) + { + return isset($this->columns[$name]) ? $this->columns[$name] : null; + } - /** - * Returns the names of all columns in this table. - * @return array list of column names - */ - public function getColumnNames() - { - return array_keys($this->columns); - } + /** + * Returns the names of all columns in this table. + * @return array list of column names + */ + public function getColumnNames() + { + return array_keys($this->columns); + } } diff --git a/extensions/sphinx/Query.php b/extensions/sphinx/Query.php index b9b9abd3894..b75abdde3fd 100644 --- a/extensions/sphinx/Query.php +++ b/extensions/sphinx/Query.php @@ -48,670 +48,700 @@ */ class Query extends Component implements QueryInterface { - use QueryTrait; - - /** - * @var array the columns being selected. For example, `['id', 'group_id']`. - * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. - * @see select() - */ - public $select; - /** - * @var string additional option that should be appended to the 'SELECT' keyword. - */ - public $selectOption; - /** - * @var boolean whether to select distinct rows of data only. If this is set true, - * the SELECT clause would be changed to SELECT DISTINCT. - */ - public $distinct; - /** - * @var array the index(es) to be selected from. For example, `['idx_user', 'idx_user_delta']`. - * This is used to construct the FROM clause in a SQL statement. - * @see from() - */ - public $from; - /** - * @var string text, which should be searched in fulltext mode. - * This value will be composed into MATCH operator inside the WHERE clause. - */ - public $match; - /** - * @var array how to group the query results. For example, `['company', 'department']`. - * This is used to construct the GROUP BY clause in a SQL statement. - */ - public $groupBy; - /** - * @var string WITHIN GROUP ORDER BY clause. This is a Sphinx specific extension - * that lets you control how the best row within a group will to be selected. - * The possible value matches the [[orderBy]] one. - */ - public $within; - /** - * @var array per-query options in format: optionName => optionValue - * They will compose OPTION clause. This is a Sphinx specific extension - * that lets you control a number of per-query options. - */ - public $options; - /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. - */ - public $params = []; - /** - * @var callable PHP callback, which should be used to fetch source data for the snippets. - * Such callback will receive array of query result rows as an argument and must return the - * array of snippet source strings in the order, which match one of incoming rows. - * For example: - * ~~~ - * $query = new Query; - * $query->from('idx_item') - * ->match('pencil') - * ->snippetCallback(function ($rows) { - * $result = []; - * foreach ($rows as $row) { - * $result[] = file_get_contents('/path/to/index/files/' . $row['id'] . '.txt'); - * } - * return $result; - * }) - * ->all(); - * ~~~ - */ - public $snippetCallback; - /** - * @var array query options for the call snippet. - */ - public $snippetOptions; - /** - * @var Connection the Sphinx connection used to generate the SQL statements. - */ - private $_connection; - - /** - * @param Connection $connection Sphinx connection instance - * @return static the query object itself - */ - public function setConnection($connection) - { - $this->_connection = $connection; - return $this; - } - - /** - * @return Connection Sphinx connection instance - */ - public function getConnection() - { - if ($this->_connection === null) { - $this->_connection = $this->defaultConnection(); - } - return $this->_connection; - } - - /** - * @return Connection default connection value. - */ - protected function defaultConnection() - { - return Yii::$app->getComponent('sphinx'); - } - - /** - * Creates a Sphinx command that can be used to execute this query. - * @param Connection $connection the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return Command the created Sphinx command instance. - */ - public function createCommand($connection = null) - { - $this->setConnection($connection); - $connection = $this->getConnection(); - list ($sql, $params) = $connection->getQueryBuilder()->build($this); - return $connection->createCommand($sql, $params); - } - - /** - * Executes the query and returns all results as an array. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $rows = $this->createCommand($db)->queryAll(); - $rows = $this->fillUpSnippets($rows); - if ($this->indexBy === null) { - return $rows; - } - $result = []; - foreach ($rows as $row) { - if (is_string($this->indexBy)) { - $key = $row[$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - $result[$key] = $row; - } - return $result; - } - - /** - * Executes the query and returns a single row of result. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - public function one($db = null) - { - $row = $this->createCommand($db)->queryOne(); - if ($row !== false) { - list ($row) = $this->fillUpSnippets([$row]); - } - return $row; - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the first column in the first row of the query results. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if the query result is empty. - */ - public function scalar($db = null) - { - return $this->createCommand($db)->queryScalar(); - } - - /** - * Executes the query and returns the first column of the result. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($db = null) - { - return $this->createCommand($db)->queryColumn(); - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. Defaults to '*'. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null) - { - $this->select = ["COUNT($q)"]; - return $this->createCommand($db)->queryScalar(); - } - - /** - * Returns the sum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return integer the sum of the specified column values - */ - public function sum($q, $db = null) - { - $this->select = ["SUM($q)"]; - return $this->createCommand($db)->queryScalar(); - } - - /** - * Returns the average of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return integer the average of the specified column values. - */ - public function average($q, $db = null) - { - $this->select = ["AVG($q)"]; - return $this->createCommand($db)->queryScalar(); - } - - /** - * Returns the minimum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return integer the minimum of the specified column values. - */ - public function min($q, $db = null) - { - $this->select = ["MIN($q)"]; - return $this->createCommand($db)->queryScalar(); - } - - /** - * Returns the maximum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return integer the maximum of the specified column values. - */ - public function max($q, $db = null) - { - $this->select = ["MAX($q)"]; - return $this->createCommand($db)->queryScalar(); - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the Sphinx connection used to generate the SQL statement. - * If this parameter is not given, the `sphinx` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - $this->select = [new Expression('1')]; - return $this->scalar($db) !== false; - } - - /** - * Sets the SELECT part of the query. - * @param string|array $columns the columns to be selected. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a Sphinx expression). - * @param string $option additional option that should be appended to the 'SELECT' keyword. - * @return static the query object itself - */ - public function select($columns, $option = null) - { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->select = $columns; - $this->selectOption = $option; - return $this; - } - - /** - * Sets the value indicating whether to SELECT DISTINCT or not. - * @param boolean $value whether to SELECT DISTINCT or not. - * @return static the query object itself - */ - public function distinct($value = true) - { - $this->distinct = $value; - return $this; - } - - /** - * Sets the FROM part of the query. - * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. `'idx_user'`) - * or an array (e.g. `['idx_user', 'idx_user_delta']`) specifying one or several index names. - * The method will automatically quote the table names unless it contains some parenthesis - * (which means the table is given as a sub-query or Sphinx expression). - * @return static the query object itself - */ - public function from($tables) - { - if (!is_array($tables)) { - $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); - } - $this->from = $tables; - return $this; - } - - /** - * Sets the fulltext query text. This text will be composed into - * MATCH operator inside the WHERE clause. - * @param string $query fulltext query text. - * @return static the query object itself - */ - public function match($query) - { - $this->match = $query; - return $this; - } - - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter, and optionally a $params parameter - * specifying the values to be bound to the query. - * - * The $condition parameter should be either a string (e.g. 'id=1') or an array. - * If the latter, it must be in one of the following two formats: - * - * - hash format: `['column1' => value1, 'column2' => value2, ...]` - * - operator format: `[operator, operand1, operand2, ...]` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. - * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. - * - `['status' => null] generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape values in the range. - * Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply - * a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. - * - * @param string|array $condition the conditions that should be put in the WHERE part. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition, $params = []) - { - $this->where = $condition; - $this->addParams($params); - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition, $params = []) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['and', $this->where, $condition]; - } - $this->addParams($params); - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition, $params = []) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['or', $this->where, $condition]; - } - $this->addParams($params); - return $this; - } - - /** - * Sets the GROUP BY part of the query. - * @param string|array $columns the columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see addGroupBy() - */ - public function groupBy($columns) - { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->groupBy = $columns; - return $this; - } - - /** - * Adds additional group-by columns to the existing ones. - * @param string|array $columns additional columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see groupBy() - */ - public function addGroupBy($columns) - { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - if ($this->groupBy === null) { - $this->groupBy = $columns; - } else { - $this->groupBy = array_merge($this->groupBy, $columns); - } - return $this; - } - - /** - * Sets the parameters to be bound to the query. - * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. - * @return static the query object itself - * @see addParams() - */ - public function params($params) - { - $this->params = $params; - return $this; - } - - /** - * Adds additional parameters to be bound to the query. - * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. - * @return static the query object itself - * @see params() - */ - public function addParams($params) - { - if (!empty($params)) { - if (empty($this->params)) { - $this->params = $params; - } else { - foreach ($params as $name => $value) { - if (is_integer($name)) { - $this->params[] = $value; - } else { - $this->params[$name] = $value; - } - } - } - } - return $this; - } - - /** - * Sets the query options. - * @param array $options query options in format: optionName => optionValue - * @return static the query object itself - * @see addOptions() - */ - public function options($options) - { - $this->options = $options; - return $this; - } - - /** - * Adds additional query options. - * @param array $options query options in format: optionName => optionValue - * @return static the query object itself - * @see options() - */ - public function addOptions($options) - { - if (is_array($this->options)) { - $this->options = array_merge($this->options, $options); - } else { - $this->options = $options; - } - return $this; - } - - /** - * Sets the WITHIN GROUP ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to find best row within a group. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see addWithin() - */ - public function within($columns) - { - $this->within = $this->normalizeOrderBy($columns); - return $this; - } - - /** - * Adds additional WITHIN GROUP ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to find best row within a group. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see within() - */ - public function addWithin($columns) - { - $columns = $this->normalizeOrderBy($columns); - if ($this->within === null) { - $this->within = $columns; - } else { - $this->within = array_merge($this->within, $columns); - } - return $this; - } - - /** - * Sets the PHP callback, which should be used to retrieve the source data - * for the snippets building. - * @param callable $callback PHP callback, which should be used to fetch source data for the snippets. - * @return static the query object itself - * @see snippetCallback - */ - public function snippetCallback($callback) - { - $this->snippetCallback = $callback; - return $this; - } - - /** - * Sets the call snippets query options. - * @param array $options call snippet options in format: option_name => option_value - * @return static the query object itself - * @see snippetCallback - */ - public function snippetOptions($options) - { - $this->snippetOptions = $options; - return $this; - } - - /** - * Fills the query result rows with the snippets built from source determined by - * [[snippetCallback]] result. - * @param array $rows raw query result rows. - * @return array|ActiveRecord[] query result rows with filled up snippets. - */ - protected function fillUpSnippets($rows) - { - if ($this->snippetCallback === null) { - return $rows; - } - $snippetSources = call_user_func($this->snippetCallback, $rows); - $snippets = $this->callSnippets($snippetSources); - $snippetKey = 0; - foreach ($rows as $key => $row) { - $rows[$key]['snippet'] = $snippets[$snippetKey]; - $snippetKey++; - } - return $rows; - } - - /** - * Builds a snippets from provided source data. - * @param array $source the source data to extract a snippet from. - * @throws InvalidCallException in case [[match]] is not specified. - * @return array snippets list. - */ - protected function callSnippets(array $source) - { - return $this->callSnippetsInternal($source, $this->from[0]); - } - - /** - * Builds a snippets from provided source data by the given index. - * @param array $source the source data to extract a snippet from. - * @param string $from name of the source index. - * @return array snippets list. - * @throws InvalidCallException in case [[match]] is not specified. - */ - protected function callSnippetsInternal(array $source, $from) - { - $connection = $this->getConnection(); - $match = $this->match; - if ($match === null) { - throw new InvalidCallException('Unable to call snippets: "' . $this->className() . '::match" should be specified.'); - } - return $connection->createCommand() - ->callSnippets($from, $source, $match, $this->snippetOptions) - ->queryColumn(); - } + use QueryTrait; + + /** + * @var array the columns being selected. For example, `['id', 'group_id']`. + * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. + * @see select() + */ + public $select; + /** + * @var string additional option that should be appended to the 'SELECT' keyword. + */ + public $selectOption; + /** + * @var boolean whether to select distinct rows of data only. If this is set true, + * the SELECT clause would be changed to SELECT DISTINCT. + */ + public $distinct; + /** + * @var array the index(es) to be selected from. For example, `['idx_user', 'idx_user_delta']`. + * This is used to construct the FROM clause in a SQL statement. + * @see from() + */ + public $from; + /** + * @var string text, which should be searched in fulltext mode. + * This value will be composed into MATCH operator inside the WHERE clause. + */ + public $match; + /** + * @var array how to group the query results. For example, `['company', 'department']`. + * This is used to construct the GROUP BY clause in a SQL statement. + */ + public $groupBy; + /** + * @var string WITHIN GROUP ORDER BY clause. This is a Sphinx specific extension + * that lets you control how the best row within a group will to be selected. + * The possible value matches the [[orderBy]] one. + */ + public $within; + /** + * @var array per-query options in format: optionName => optionValue + * They will compose OPTION clause. This is a Sphinx specific extension + * that lets you control a number of per-query options. + */ + public $options; + /** + * @var array list of query parameter values indexed by parameter placeholders. + * For example, `[':name' => 'Dan', ':age' => 31]`. + */ + public $params = []; + /** + * @var callable PHP callback, which should be used to fetch source data for the snippets. + * Such callback will receive array of query result rows as an argument and must return the + * array of snippet source strings in the order, which match one of incoming rows. + * For example: + * ~~~ + * $query = new Query; + * $query->from('idx_item') + * ->match('pencil') + * ->snippetCallback(function ($rows) { + * $result = []; + * foreach ($rows as $row) { + * $result[] = file_get_contents('/path/to/index/files/' . $row['id'] . '.txt'); + * } + * return $result; + * }) + * ->all(); + * ~~~ + */ + public $snippetCallback; + /** + * @var array query options for the call snippet. + */ + public $snippetOptions; + /** + * @var Connection the Sphinx connection used to generate the SQL statements. + */ + private $_connection; + + /** + * @param Connection $connection Sphinx connection instance + * @return static the query object itself + */ + public function setConnection($connection) + { + $this->_connection = $connection; + + return $this; + } + + /** + * @return Connection Sphinx connection instance + */ + public function getConnection() + { + if ($this->_connection === null) { + $this->_connection = $this->defaultConnection(); + } + + return $this->_connection; + } + + /** + * @return Connection default connection value. + */ + protected function defaultConnection() + { + return Yii::$app->getComponent('sphinx'); + } + + /** + * Creates a Sphinx command that can be used to execute this query. + * @param Connection $connection the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return Command the created Sphinx command instance. + */ + public function createCommand($connection = null) + { + $this->setConnection($connection); + $connection = $this->getConnection(); + list ($sql, $params) = $connection->getQueryBuilder()->build($this); + + return $connection->createCommand($sql, $params); + } + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $rows = $this->createCommand($db)->queryAll(); + $rows = $this->fillUpSnippets($rows); + if ($this->indexBy === null) { + return $rows; + } + $result = []; + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $result[$key] = $row; + } + + return $result; + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null) + { + $row = $this->createCommand($db)->queryOne(); + if ($row !== false) { + list ($row) = $this->fillUpSnippets([$row]); + } + + return $row; + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the first column in the first row of the query results. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if the query result is empty. + */ + public function scalar($db = null) + { + return $this->createCommand($db)->queryScalar(); + } + + /** + * Executes the query and returns the first column of the result. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($db = null) + { + return $this->createCommand($db)->queryColumn(); + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null) + { + $this->select = ["COUNT($q)"]; + + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the sum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return integer the sum of the specified column values + */ + public function sum($q, $db = null) + { + $this->select = ["SUM($q)"]; + + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the average of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($q, $db = null) + { + $this->select = ["AVG($q)"]; + + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the minimum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($q, $db = null) + { + $this->select = ["MIN($q)"]; + + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns the maximum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($q, $db = null) + { + $this->select = ["MAX($q)"]; + + return $this->createCommand($db)->queryScalar(); + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the Sphinx connection used to generate the SQL statement. + * If this parameter is not given, the `sphinx` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + $this->select = [new Expression('1')]; + + return $this->scalar($db) !== false; + } + + /** + * Sets the SELECT part of the query. + * @param string|array $columns the columns to be selected. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a Sphinx expression). + * @param string $option additional option that should be appended to the 'SELECT' keyword. + * @return static the query object itself + */ + public function select($columns, $option = null) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->select = $columns; + $this->selectOption = $option; + + return $this; + } + + /** + * Sets the value indicating whether to SELECT DISTINCT or not. + * @param boolean $value whether to SELECT DISTINCT or not. + * @return static the query object itself + */ + public function distinct($value = true) + { + $this->distinct = $value; + + return $this; + } + + /** + * Sets the FROM part of the query. + * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. `'idx_user'`) + * or an array (e.g. `['idx_user', 'idx_user_delta']`) specifying one or several index names. + * The method will automatically quote the table names unless it contains some parenthesis + * (which means the table is given as a sub-query or Sphinx expression). + * @return static the query object itself + */ + public function from($tables) + { + if (!is_array($tables)) { + $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); + } + $this->from = $tables; + + return $this; + } + + /** + * Sets the fulltext query text. This text will be composed into + * MATCH operator inside the WHERE clause. + * @param string $query fulltext query text. + * @return static the query object itself + */ + public function match($query) + { + $this->match = $query; + + return $this; + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $condition parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be in one of the following two formats: + * + * - hash format: `['column1' => value1, 'column2' => value2, ...]` + * - operator format: `[operator, operand1, operand2, ...]` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. + * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `['status' => null] generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape values in the range. + * Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply + * a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param string|array $condition the conditions that should be put in the WHERE part. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition, $params = []) + { + $this->where = $condition; + $this->addParams($params); + + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition, $params = []) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['and', $this->where, $condition]; + } + $this->addParams($params); + + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition, $params = []) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['or', $this->where, $condition]; + } + $this->addParams($params); + + return $this; + } + + /** + * Sets the GROUP BY part of the query. + * @param string|array $columns the columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addGroupBy() + */ + public function groupBy($columns) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->groupBy = $columns; + + return $this; + } + + /** + * Adds additional group-by columns to the existing ones. + * @param string|array $columns additional columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see groupBy() + */ + public function addGroupBy($columns) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + if ($this->groupBy === null) { + $this->groupBy = $columns; + } else { + $this->groupBy = array_merge($this->groupBy, $columns); + } + + return $this; + } + + /** + * Sets the parameters to be bound to the query. + * @param array $params list of query parameter values indexed by parameter placeholders. + * For example, `[':name' => 'Dan', ':age' => 31]`. + * @return static the query object itself + * @see addParams() + */ + public function params($params) + { + $this->params = $params; + + return $this; + } + + /** + * Adds additional parameters to be bound to the query. + * @param array $params list of query parameter values indexed by parameter placeholders. + * For example, `[':name' => 'Dan', ':age' => 31]`. + * @return static the query object itself + * @see params() + */ + public function addParams($params) + { + if (!empty($params)) { + if (empty($this->params)) { + $this->params = $params; + } else { + foreach ($params as $name => $value) { + if (is_integer($name)) { + $this->params[] = $value; + } else { + $this->params[$name] = $value; + } + } + } + } + + return $this; + } + + /** + * Sets the query options. + * @param array $options query options in format: optionName => optionValue + * @return static the query object itself + * @see addOptions() + */ + public function options($options) + { + $this->options = $options; + + return $this; + } + + /** + * Adds additional query options. + * @param array $options query options in format: optionName => optionValue + * @return static the query object itself + * @see options() + */ + public function addOptions($options) + { + if (is_array($this->options)) { + $this->options = array_merge($this->options, $options); + } else { + $this->options = $options; + } + + return $this; + } + + /** + * Sets the WITHIN GROUP ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to find best row within a group. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addWithin() + */ + public function within($columns) + { + $this->within = $this->normalizeOrderBy($columns); + + return $this; + } + + /** + * Adds additional WITHIN GROUP ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to find best row within a group. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see within() + */ + public function addWithin($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->within === null) { + $this->within = $columns; + } else { + $this->within = array_merge($this->within, $columns); + } + + return $this; + } + + /** + * Sets the PHP callback, which should be used to retrieve the source data + * for the snippets building. + * @param callable $callback PHP callback, which should be used to fetch source data for the snippets. + * @return static the query object itself + * @see snippetCallback + */ + public function snippetCallback($callback) + { + $this->snippetCallback = $callback; + + return $this; + } + + /** + * Sets the call snippets query options. + * @param array $options call snippet options in format: option_name => option_value + * @return static the query object itself + * @see snippetCallback + */ + public function snippetOptions($options) + { + $this->snippetOptions = $options; + + return $this; + } + + /** + * Fills the query result rows with the snippets built from source determined by + * [[snippetCallback]] result. + * @param array $rows raw query result rows. + * @return array|ActiveRecord[] query result rows with filled up snippets. + */ + protected function fillUpSnippets($rows) + { + if ($this->snippetCallback === null) { + return $rows; + } + $snippetSources = call_user_func($this->snippetCallback, $rows); + $snippets = $this->callSnippets($snippetSources); + $snippetKey = 0; + foreach ($rows as $key => $row) { + $rows[$key]['snippet'] = $snippets[$snippetKey]; + $snippetKey++; + } + + return $rows; + } + + /** + * Builds a snippets from provided source data. + * @param array $source the source data to extract a snippet from. + * @throws InvalidCallException in case [[match]] is not specified. + * @return array snippets list. + */ + protected function callSnippets(array $source) + { + return $this->callSnippetsInternal($source, $this->from[0]); + } + + /** + * Builds a snippets from provided source data by the given index. + * @param array $source the source data to extract a snippet from. + * @param string $from name of the source index. + * @return array snippets list. + * @throws InvalidCallException in case [[match]] is not specified. + */ + protected function callSnippetsInternal(array $source, $from) + { + $connection = $this->getConnection(); + $match = $this->match; + if ($match === null) { + throw new InvalidCallException('Unable to call snippets: "' . $this->className() . '::match" should be specified.'); + } + + return $connection->createCommand() + ->callSnippets($from, $source, $match, $this->snippetOptions) + ->queryColumn(); + } } diff --git a/extensions/sphinx/QueryBuilder.php b/extensions/sphinx/QueryBuilder.php index 6f99bc8d2a2..9bb1adcce6d 100644 --- a/extensions/sphinx/QueryBuilder.php +++ b/extensions/sphinx/QueryBuilder.php @@ -23,922 +23,940 @@ */ class QueryBuilder extends Object { - /** - * The prefix for automatically generated query binding parameters. - */ - const PARAM_PREFIX = ':qp'; - - /** - * @var Connection the Sphinx connection. - */ - public $db; - /** - * @var string the separator between different fragments of a SQL statement. - * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement. - */ - public $separator = " "; - - /** - * Constructor. - * @param Connection $connection the Sphinx connection. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($connection, $config = []) - { - $this->db = $connection; - parent::__construct($config); - } - - /** - * Generates a SELECT SQL statement from a [[Query]] object. - * @param Query $query the [[Query]] object from which the SQL statement will be generated - * @param array $params the parameters to be bound to the generated SQL statement. These parameters will - * be included in the result with the additional parameters generated during the query building process. - * @return array the generated SQL statement (the first array element) and the corresponding - * parameters to be bound to the SQL statement (the second array element). The parameters returned - * include those provided in `$params`. - */ - public function build($query, $params = []) - { - $params = empty($params) ? $query->params : array_merge($params, $query->params); - - if ($query->match !== null) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = (string)$query->match; - $query->andWhere('MATCH(' . $phName . ')'); - } - - $from = $query->from; - if ($from === null && $query instanceof ActiveQuery) { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $from = [$modelClass::indexName()]; - } - - $clauses = [ - $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption), - $this->buildFrom($from, $params), - $this->buildWhere($query->from, $query->where, $params), - $this->buildGroupBy($query->groupBy), - $this->buildWithin($query->within), - $this->buildOrderBy($query->orderBy), - $this->buildLimit($query->limit, $query->offset), - $this->buildOption($query->options, $params), - ]; - return [implode($this->separator, array_filter($clauses)), $params]; - } - - /** - * Creates an INSERT SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->insert('idx_user', [ - * 'name' => 'Sam', - * 'age' => 30, - * 'id' => 10, - * ], $params); - * ~~~ - * - * The method will properly escape the index and column names. - * - * @param string $index the index that new rows will be inserted into. - * @param array $columns the column data (name => value) to be inserted into the index. - * @param array $params the binding parameters that will be generated by this method. - * They should be bound to the Sphinx command later. - * @return string the INSERT SQL - */ - public function insert($index, $columns, &$params) - { - return $this->generateInsertReplace('INSERT', $index, $columns, $params); - } - - /** - * Creates an REPLACE SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->replace('idx_user', [ - * 'name' => 'Sam', - * 'age' => 30, - * 'id' => 10, - * ], $params); - * ~~~ - * - * The method will properly escape the index and column names. - * - * @param string $index the index that new rows will be replaced. - * @param array $columns the column data (name => value) to be replaced in the index. - * @param array $params the binding parameters that will be generated by this method. - * They should be bound to the Sphinx command later. - * @return string the INSERT SQL - */ - public function replace($index, $columns, &$params) - { - return $this->generateInsertReplace('REPLACE', $index, $columns, $params); - } - - /** - * Generates INSERT/REPLACE SQL statement. - * @param string $statement statement ot be generated. - * @param string $index the affected index name. - * @param array $columns the column data (name => value). - * @param array $params the binding parameters that will be generated by this method. - * @return string generated SQL - */ - protected function generateInsertReplace($statement, $index, $columns, &$params) - { - if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { - $indexSchemas = [$indexSchema]; - } else { - $indexSchemas = []; - } - $names = []; - $placeholders = []; - foreach ($columns as $name => $value) { - $names[] = $this->db->quoteColumnName($name); - $placeholders[] = $this->composeColumnValue($indexSchemas, $name, $value, $params); - } - return $statement . ' INTO ' . $this->db->quoteIndexName($index) - . ' (' . implode(', ', $names) . ') VALUES (' - . implode(', ', $placeholders) . ')'; - } - - /** - * Generates a batch INSERT SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->batchInsert('idx_user', ['id', 'name', 'age'], [ - * [1, 'Tom', 30], - * [2, 'Jane', 20], - * [3, 'Linda', 25], - * ], $params); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $index the index that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the index - * @param array $params the binding parameters that will be generated by this method. - * They should be bound to the Sphinx command later. - * @return string the batch INSERT SQL statement - */ - public function batchInsert($index, $columns, $rows, &$params) - { - return $this->generateBatchInsertReplace('INSERT', $index, $columns, $rows, $params); - } - - /** - * Generates a batch REPLACE SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->batchReplace('idx_user', ['id', 'name', 'age'], [ - * [1, 'Tom', 30], - * [2, 'Jane', 20], - * [3, 'Linda', 25], - * ], $params); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $index the index that new rows will be replaced. - * @param array $columns the column names - * @param array $rows the rows to be batch replaced in the index - * @param array $params the binding parameters that will be generated by this method. - * They should be bound to the Sphinx command later. - * @return string the batch INSERT SQL statement - */ - public function batchReplace($index, $columns, $rows, &$params) - { - return $this->generateBatchInsertReplace('REPLACE', $index, $columns, $rows, $params); - } - - /** - * Generates a batch INSERT/REPLACE SQL statement. - * @param string $statement statement ot be generated. - * @param string $index the affected index name. - * @param array $columns the column data (name => value). - * @param array $rows the rows to be batch inserted into the index - * @param array $params the binding parameters that will be generated by this method. - * @return string generated SQL - */ - protected function generateBatchInsertReplace($statement, $index, $columns, $rows, &$params) - { - if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { - $indexSchemas = [$indexSchema]; - } else { - $indexSchemas = []; - } - - foreach ($columns as $i => $name) { - $columns[$i] = $this->db->quoteColumnName($name); - } - - $values = []; - foreach ($rows as $row) { - $vs = []; - foreach ($row as $i => $value) { - $vs[] = $this->composeColumnValue($indexSchemas, $columns[$i], $value, $params); - } - $values[] = '(' . implode(', ', $vs) . ')'; - } - - return $statement . ' INTO ' . $this->db->quoteIndexName($index) - . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); - } - - /** - * Creates an UPDATE SQL statement. - * For example, - * - * ~~~ - * $params = []; - * $sql = $queryBuilder->update('idx_user', ['status' => 1], 'age > 30', $params); - * ~~~ - * - * The method will properly escape the index and column names. - * - * @param string $index the index to be updated. - * @param array $columns the column data (name => value) to be updated. - * @param array|string $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the binding parameters that will be modified by this method - * so that they can be bound to the Sphinx command later. - * @param array $options list of options in format: optionName => optionValue - * @return string the UPDATE SQL - */ - public function update($index, $columns, $condition, &$params, $options) - { - if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { - $indexSchemas = [$indexSchema]; - } else { - $indexSchemas = []; - } - - $lines = []; - foreach ($columns as $name => $value) { - $lines[] = $this->db->quoteColumnName($name) . '=' . $this->composeColumnValue($indexSchemas, $name, $value, $params); - } - - $sql = 'UPDATE ' . $this->db->quoteIndexName($index) . ' SET ' . implode(', ', $lines); - $where = $this->buildWhere([$index], $condition, $params); - if ($where !== '') { - $sql = $sql . ' ' . $where; - } - $option = $this->buildOption($options, $params); - if ($option !== '') { - $sql = $sql . ' ' . $option; - } - return $sql; - } - - /** - * Creates a DELETE SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->delete('idx_user', 'status = 0'); - * ~~~ - * - * The method will properly escape the index and column names. - * - * @param string $index the index where the data will be deleted from. - * @param array|string $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the binding parameters that will be modified by this method - * so that they can be bound to the Sphinx command later. - * @return string the DELETE SQL - */ - public function delete($index, $condition, &$params) - { - $sql = 'DELETE FROM ' . $this->db->quoteIndexName($index); - $where = $this->buildWhere([$index], $condition, $params); - return $where === '' ? $sql : $sql . ' ' . $where; - } - - /** - * Builds a SQL statement for truncating an index. - * @param string $index the index to be truncated. The name will be properly quoted by the method. - * @return string the SQL statement for truncating an index. - */ - public function truncateIndex($index) - { - return 'TRUNCATE RTINDEX ' . $this->db->quoteIndexName($index); - } - - /** - * Builds a SQL statement for call snippet from provided data and query, using specified index settings. - * @param string $index name of the index, from which to take the text processing settings. - * @param string|array $source is the source data to extract a snippet from. - * It could be either a single string or array of strings. - * @param string $match the full-text query to build snippets for. - * @param array $options list of options in format: optionName => optionValue - * @param array $params the binding parameters that will be modified by this method - * so that they can be bound to the Sphinx command later. - * @return string the SQL statement for call snippets. - */ - public function callSnippets($index, $source, $match, $options, &$params) - { - if (is_array($source)) { - $dataSqlParts = []; - foreach ($source as $sourceRow) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $sourceRow; - $dataSqlParts[] = $phName; - } - $dataSql = '(' . implode(',', $dataSqlParts) . ')'; - } else { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $source; - $dataSql = $phName; - } - $indexParamName = self::PARAM_PREFIX . count($params); - $params[$indexParamName] = $index; - $matchParamName = self::PARAM_PREFIX . count($params); - $params[$matchParamName] = $match; - if (!empty($options)) { - $optionParts = []; - foreach ($options as $name => $value) { - if ($value instanceof Expression) { - $actualValue = $value->expression; - } else { - $actualValue = self::PARAM_PREFIX . count($params); - $params[$actualValue] = $value; - } - $optionParts[] = $actualValue . ' AS ' . $name; - } - $optionSql = ', ' . implode(', ', $optionParts); - } else { - $optionSql = ''; - } - return 'CALL SNIPPETS(' . $dataSql. ', ' . $indexParamName . ', ' . $matchParamName . $optionSql. ')'; - } - - /** - * Builds a SQL statement for returning tokenized and normalized forms of the keywords, and, - * optionally, keyword statistics. - * @param string $index the name of the index from which to take the text processing settings - * @param string $text the text to break down to keywords. - * @param boolean $fetchStatistic whether to return document and hit occurrence statistics - * @param array $params the binding parameters that will be modified by this method - * so that they can be bound to the Sphinx command later. - * @return string the SQL statement for call keywords. - */ - public function callKeywords($index, $text, $fetchStatistic, &$params) - { - $indexParamName = self::PARAM_PREFIX . count($params); - $params[$indexParamName] = $index; - $textParamName = self::PARAM_PREFIX . count($params); - $params[$textParamName] = $text; - return 'CALL KEYWORDS(' . $textParamName . ', ' . $indexParamName . ($fetchStatistic ? ', 1' : '') . ')'; - } - - /** - * @param array $columns - * @param array $params the binding parameters to be populated - * @param boolean $distinct - * @param string $selectOption - * @return string the SELECT clause built from [[query]]. - */ - public function buildSelect($columns, &$params, $distinct = false, $selectOption = null) - { - $select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; - if ($selectOption !== null) { - $select .= ' ' . $selectOption; - } - - if (empty($columns)) { - return $select . ' *'; - } - - foreach ($columns as $i => $column) { - if ($column instanceof Expression) { - $columns[$i] = $column->expression; - $params = array_merge($params, $column->params); - } elseif (is_string($i)) { - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - $columns[$i] = "$column AS " . $this->db->quoteColumnName($i); - } elseif (strpos($column, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { - $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); - } else { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - } - - return $select . ' ' . implode(', ', $columns); - } - - /** - * @param array $indexes - * @param array $params the binding parameters to be populated - * @return string the FROM clause built from [[query]]. - */ - public function buildFrom($indexes, &$params) - { - if (empty($indexes)) { - return ''; - } - - foreach ($indexes as $i => $index) { - if ($index instanceof Query) { - list($sql, $params) = $this->build($index, $params); - $indexes[$i] = "($sql) " . $this->db->quoteIndexName($i); - } elseif (is_string($i)) { - if (strpos($index, '(') === false) { - $index = $this->db->quoteIndexName($index); - } - $indexes[$i] = "$index " . $this->db->quoteIndexName($i); - } elseif (strpos($index, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $index, $matches)) { // with alias - $indexes[$i] = $this->db->quoteIndexName($matches[1]) . ' ' . $this->db->quoteIndexName($matches[2]); - } else { - $indexes[$i] = $this->db->quoteIndexName($index); - } - } - } - - if (is_array($indexes)) { - $indexes = implode(', ', $indexes); - } - - return 'FROM ' . $indexes; - } - - /** - * @param string[] $indexes list of index names, which affected by query - * @param string|array $condition - * @param array $params the binding parameters to be populated - * @return string the WHERE clause built from [[query]]. - */ - public function buildWhere($indexes, $condition, &$params) - { - if (empty($condition)) { - return ''; - } - $indexSchemas = []; - if (!empty($indexes)) { - foreach ($indexes as $indexName) { - $index = $this->db->getIndexSchema($indexName); - if ($index !== null) { - $indexSchemas[] = $index; - } - } - } - $where = $this->buildCondition($indexSchemas, $condition, $params); - return $where === '' ? '' : 'WHERE ' . $where; - } - - /** - * @param array $columns - * @return string the GROUP BY clause - */ - public function buildGroupBy($columns) - { - return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); - } - - /** - * @param array $columns - * @return string the ORDER BY clause built from [[query]]. - */ - public function buildOrderBy($columns) - { - if (empty($columns)) { - return ''; - } - $orders = []; - foreach ($columns as $name => $direction) { - if ($direction instanceof Expression) { - $orders[] = $direction->expression; - } else { - $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : 'ASC'); - } - } - - return 'ORDER BY ' . implode(', ', $orders); - } - - /** - * @param integer $limit - * @param integer $offset - * @return string the LIMIT and OFFSET clauses built from [[query]]. - */ - public function buildLimit($limit, $offset) - { - $sql = ''; - if (is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0') { - $sql = 'LIMIT ' . $offset; - } - if (is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0) { - $sql = $sql === '' ? "LIMIT $limit" : "$sql,$limit"; - } elseif ($sql !== '') { - $sql .= ',1000'; // this is the default limit by sphinx - } - - return $sql; - } - - /** - * Processes columns and properly quote them if necessary. - * It will join all columns into a string with comma as separators. - * @param string|array $columns the columns to be processed - * @return string the processing result - */ - public function buildColumns($columns) - { - if (!is_array($columns)) { - if (strpos($columns, '(') !== false) { - return $columns; - } else { - $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); - } - } - foreach ($columns as $i => $column) { - if ($column instanceof Expression) { - $columns[$i] = $column->expression; - } elseif (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - return is_array($columns) ? implode(', ', $columns) : $columns; - } - - /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws \yii\db\Exception if the condition is in bad format - */ - public function buildCondition($indexes, $condition, &$params) - { - static $builders = [ - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'NOT LIKE' => 'buildLikeCondition', - 'OR LIKE' => 'buildLikeCondition', - 'OR NOT LIKE' => 'buildLikeCondition', - ]; - - if (!is_array($condition)) { - return (string)$condition; - } elseif (empty($condition)) { - return ''; - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($indexes, $operator, $condition, $params); - } else { - throw new Exception('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($indexes, $condition, $params); - } - } - - /** - * Creates a condition based on column-value pairs. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param array $condition the condition specification. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - */ - public function buildHashCondition($indexes, $condition, &$params) - { - $parts = []; - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition($indexes, 'IN', [$column, $value], $params); - } else { - if (strpos($column, '(') === false) { - $quotedColumn = $this->db->quoteColumnName($column); - } else { - $quotedColumn = $column; - } - if ($value === null) { - $parts[] = "$quotedColumn IS NULL"; - } else { - $parts[] = $quotedColumn . '=' . $this->composeColumnValue($indexes, $column, $value, $params); - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; - } - - /** - * Connects two or more SQL expressions with the `AND` or `OR` operator. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the SQL expressions to connect. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - */ - public function buildAndCondition($indexes, $operator, $operands, &$params) - { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($indexes, $operand, $params); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - /** - * Creates an SQL expressions with the `BETWEEN` operator. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) - * @param array $operands the first operand is the column name. The second and third operands - * describe the interval that column value should be in. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws Exception if wrong number of operands have been given. - */ - public function buildBetweenCondition($indexes, $operator, $operands, &$params) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - if (strpos($column, '(') === false) { - $quotedColumn = $this->db->quoteColumnName($column); - } else { - $quotedColumn = $column; - } - $phName1 = $this->composeColumnValue($indexes, $column, $value1, $params); - $phName2 = $this->composeColumnValue($indexes, $column, $value2, $params); - - return "$quotedColumn $operator $phName1 AND $phName2"; - } - - /** - * Creates an SQL expressions with the `IN` operator. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) - * @param array $operands the first operand is the column name. If it is an array - * a composite IN condition will be generated. - * The second operand is an array of values that column value should be among. - * If it is an empty array the generated expression will be a `false` value if - * operator is `IN` and empty if operator is `NOT IN`. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws Exception if wrong number of operands have been given. - */ - public function buildInCondition($indexes, $operator, $operands, &$params) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'IN' ? '0=1' : ''; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($indexes, $operator, $column, $values, $params); - } elseif (is_array($column)) { - $column = reset($column); - } - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - $values[$i] = $this->composeColumnValue($indexes, $column, $value, $params); - } - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - if (count($values) > 1) { - return "$column $operator (" . implode(', ', $values) . ')'; - } else { - $operator = $operator === 'IN' ? '=' : '<>'; - return $column . $operator . reset($values); - } - } - - /** - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) - * @param array $columns - * @param array $values - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - */ - protected function buildCompositeInCondition($indexes, $operator, $columns, $values, &$params) - { - $vss = []; - foreach ($values as $value) { - $vs = []; - foreach ($columns as $column) { - if (isset($value[$column])) { - $vs[] = $this->composeColumnValue($indexes, $column, $value[$column], $params); - } else { - $vs[] = 'NULL'; - } - } - $vss[] = '(' . implode(', ', $vs) . ')'; - } - foreach ($columns as $i => $column) { - if (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; - } - - /** - * Creates an SQL expressions with the `LIKE` operator. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) - * @param array $operands an array of two or three operands - * - * - The first operand is the column name. - * - The second operand is a single value or an array of values that column value - * should be compared with. If it is an empty array the generated expression will - * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator - * is `NOT LIKE` or `OR NOT LIKE`. - * - An optional third operand can also be provided to specify how to escape special characters - * in the value(s). The operand should be an array of mappings from the special characters to their - * escaped counterparts. If this operand is not provided, a default escape mapping will be used. - * You may use `false` or an empty array to indicate the values are already escaped and no escape - * should be applied. Note that when using an escape mapping (or the third operand is not provided), - * the values will be automatically enclosed within a pair of percentage characters. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildLikeCondition($indexes, $operator, $operands, &$params) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']; - unset($operands[2]); - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values)) { - return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; - } - - if ($operator === 'LIKE' || $operator === 'NOT LIKE') { - $andor = ' AND '; - } else { - $andor = ' OR '; - $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; - } - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - $parts = []; - foreach ($values as $value) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); - $parts[] = "$column $operator $phName"; - } - - return implode($andor, $parts); - } - - /** - * @param array $columns - * @return string the ORDER BY clause built from [[query]]. - */ - public function buildWithin($columns) - { - if (empty($columns)) { - return ''; - } - $orders = []; - foreach ($columns as $name => $direction) { - if ($direction instanceof Expression) { - $orders[] = $direction->expression; - } else { - $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); - } - } - return 'WITHIN GROUP ORDER BY ' . implode(', ', $orders); - } - - /** - * @param array $options query options in format: optionName => optionValue - * @param array $params the binding parameters to be populated - * @return string the OPTION clause build from [[query]] - */ - public function buildOption($options, &$params) - { - if (empty($options)) { - return ''; - } - $optionLines = []; - foreach ($options as $name => $value) { - if ($value instanceof Expression) { - $actualValue = $value->expression; - } else { - if (is_array($value)) { - $actualValueParts = []; - foreach ($value as $key => $valuePart) { - if (is_numeric($key)) { - $actualValuePart = ''; - } else { - $actualValuePart = $key . ' = '; - } - if ($valuePart instanceof Expression) { - $actualValuePart .= $valuePart->expression; - } else { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $valuePart; - $actualValuePart .= $phName; - } - $actualValueParts[] = $actualValuePart; - } - $actualValue = '(' . implode(', ', $actualValueParts) . ')'; - } else { - $actualValue = self::PARAM_PREFIX . count($params); - $params[$actualValue] = $value; - } - } - $optionLines[] = $name . ' = ' . $actualValue; - } - return 'OPTION ' . implode(', ', $optionLines); - } - - /** - * Composes column value for SQL, taking in account the column type. - * @param IndexSchema[] $indexes list of indexes, which affected by query - * @param string $columnName name of the column - * @param mixed $value raw column value - * @param array $params the binding parameters to be populated - * @return string SQL expression, which represents column value - */ - protected function composeColumnValue($indexes, $columnName, $value, &$params) - { - if ($value === null) { - return 'NULL'; - } elseif ($value instanceof Expression) { - $params = array_merge($params, $value->params); - return $value->expression; - } - foreach ($indexes as $index) { - $columnSchema = $index->getColumn($columnName); - if ($columnSchema !== null) { - break; - } - } - if (is_array($value)) { - // MVA : - $lineParts = []; - foreach ($value as $subValue) { - if ($subValue instanceof Expression) { - $params = array_merge($params, $subValue->params); - $lineParts[] = $subValue->expression; - } else { - $phName = self::PARAM_PREFIX . count($params); - $lineParts[] = $phName; - $params[$phName] = (isset($columnSchema)) ? $columnSchema->typecast($subValue) : $subValue; - } - } - return '(' . implode(',', $lineParts) . ')'; - } else { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = (isset($columnSchema)) ? $columnSchema->typecast($value) : $value; - return $phName; - } - } + /** + * The prefix for automatically generated query binding parameters. + */ + const PARAM_PREFIX = ':qp'; + + /** + * @var Connection the Sphinx connection. + */ + public $db; + /** + * @var string the separator between different fragments of a SQL statement. + * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement. + */ + public $separator = " "; + + /** + * Constructor. + * @param Connection $connection the Sphinx connection. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($connection, $config = []) + { + $this->db = $connection; + parent::__construct($config); + } + + /** + * Generates a SELECT SQL statement from a [[Query]] object. + * @param Query $query the [[Query]] object from which the SQL statement will be generated + * @param array $params the parameters to be bound to the generated SQL statement. These parameters will + * be included in the result with the additional parameters generated during the query building process. + * @return array the generated SQL statement (the first array element) and the corresponding + * parameters to be bound to the SQL statement (the second array element). The parameters returned + * include those provided in `$params`. + */ + public function build($query, $params = []) + { + $params = empty($params) ? $query->params : array_merge($params, $query->params); + + if ($query->match !== null) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = (string) $query->match; + $query->andWhere('MATCH(' . $phName . ')'); + } + + $from = $query->from; + if ($from === null && $query instanceof ActiveQuery) { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $from = [$modelClass::indexName()]; + } + + $clauses = [ + $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption), + $this->buildFrom($from, $params), + $this->buildWhere($query->from, $query->where, $params), + $this->buildGroupBy($query->groupBy), + $this->buildWithin($query->within), + $this->buildOrderBy($query->orderBy), + $this->buildLimit($query->limit, $query->offset), + $this->buildOption($query->options, $params), + ]; + + return [implode($this->separator, array_filter($clauses)), $params]; + } + + /** + * Creates an INSERT SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->insert('idx_user', [ + * 'name' => 'Sam', + * 'age' => 30, + * 'id' => 10, + * ], $params); + * ~~~ + * + * The method will properly escape the index and column names. + * + * @param string $index the index that new rows will be inserted into. + * @param array $columns the column data (name => value) to be inserted into the index. + * @param array $params the binding parameters that will be generated by this method. + * They should be bound to the Sphinx command later. + * @return string the INSERT SQL + */ + public function insert($index, $columns, &$params) + { + return $this->generateInsertReplace('INSERT', $index, $columns, $params); + } + + /** + * Creates an REPLACE SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->replace('idx_user', [ + * 'name' => 'Sam', + * 'age' => 30, + * 'id' => 10, + * ], $params); + * ~~~ + * + * The method will properly escape the index and column names. + * + * @param string $index the index that new rows will be replaced. + * @param array $columns the column data (name => value) to be replaced in the index. + * @param array $params the binding parameters that will be generated by this method. + * They should be bound to the Sphinx command later. + * @return string the INSERT SQL + */ + public function replace($index, $columns, &$params) + { + return $this->generateInsertReplace('REPLACE', $index, $columns, $params); + } + + /** + * Generates INSERT/REPLACE SQL statement. + * @param string $statement statement ot be generated. + * @param string $index the affected index name. + * @param array $columns the column data (name => value). + * @param array $params the binding parameters that will be generated by this method. + * @return string generated SQL + */ + protected function generateInsertReplace($statement, $index, $columns, &$params) + { + if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { + $indexSchemas = [$indexSchema]; + } else { + $indexSchemas = []; + } + $names = []; + $placeholders = []; + foreach ($columns as $name => $value) { + $names[] = $this->db->quoteColumnName($name); + $placeholders[] = $this->composeColumnValue($indexSchemas, $name, $value, $params); + } + + return $statement . ' INTO ' . $this->db->quoteIndexName($index) + . ' (' . implode(', ', $names) . ') VALUES (' + . implode(', ', $placeholders) . ')'; + } + + /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->batchInsert('idx_user', ['id', 'name', 'age'], [ + * [1, 'Tom', 30], + * [2, 'Jane', 20], + * [3, 'Linda', 25], + * ], $params); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $index the index that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the index + * @param array $params the binding parameters that will be generated by this method. + * They should be bound to the Sphinx command later. + * @return string the batch INSERT SQL statement + */ + public function batchInsert($index, $columns, $rows, &$params) + { + return $this->generateBatchInsertReplace('INSERT', $index, $columns, $rows, $params); + } + + /** + * Generates a batch REPLACE SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->batchReplace('idx_user', ['id', 'name', 'age'], [ + * [1, 'Tom', 30], + * [2, 'Jane', 20], + * [3, 'Linda', 25], + * ], $params); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $index the index that new rows will be replaced. + * @param array $columns the column names + * @param array $rows the rows to be batch replaced in the index + * @param array $params the binding parameters that will be generated by this method. + * They should be bound to the Sphinx command later. + * @return string the batch INSERT SQL statement + */ + public function batchReplace($index, $columns, $rows, &$params) + { + return $this->generateBatchInsertReplace('REPLACE', $index, $columns, $rows, $params); + } + + /** + * Generates a batch INSERT/REPLACE SQL statement. + * @param string $statement statement ot be generated. + * @param string $index the affected index name. + * @param array $columns the column data (name => value). + * @param array $rows the rows to be batch inserted into the index + * @param array $params the binding parameters that will be generated by this method. + * @return string generated SQL + */ + protected function generateBatchInsertReplace($statement, $index, $columns, $rows, &$params) + { + if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { + $indexSchemas = [$indexSchema]; + } else { + $indexSchemas = []; + } + + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } + + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + $vs[] = $this->composeColumnValue($indexSchemas, $columns[$i], $value, $params); + } + $values[] = '(' . implode(', ', $vs) . ')'; + } + + return $statement . ' INTO ' . $this->db->quoteIndexName($index) + . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + } + + /** + * Creates an UPDATE SQL statement. + * For example, + * + * ~~~ + * $params = []; + * $sql = $queryBuilder->update('idx_user', ['status' => 1], 'age > 30', $params); + * ~~~ + * + * The method will properly escape the index and column names. + * + * @param string $index the index to be updated. + * @param array $columns the column data (name => value) to be updated. + * @param array|string $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the Sphinx command later. + * @param array $options list of options in format: optionName => optionValue + * @return string the UPDATE SQL + */ + public function update($index, $columns, $condition, &$params, $options) + { + if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { + $indexSchemas = [$indexSchema]; + } else { + $indexSchemas = []; + } + + $lines = []; + foreach ($columns as $name => $value) { + $lines[] = $this->db->quoteColumnName($name) . '=' . $this->composeColumnValue($indexSchemas, $name, $value, $params); + } + + $sql = 'UPDATE ' . $this->db->quoteIndexName($index) . ' SET ' . implode(', ', $lines); + $where = $this->buildWhere([$index], $condition, $params); + if ($where !== '') { + $sql = $sql . ' ' . $where; + } + $option = $this->buildOption($options, $params); + if ($option !== '') { + $sql = $sql . ' ' . $option; + } + + return $sql; + } + + /** + * Creates a DELETE SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->delete('idx_user', 'status = 0'); + * ~~~ + * + * The method will properly escape the index and column names. + * + * @param string $index the index where the data will be deleted from. + * @param array|string $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the Sphinx command later. + * @return string the DELETE SQL + */ + public function delete($index, $condition, &$params) + { + $sql = 'DELETE FROM ' . $this->db->quoteIndexName($index); + $where = $this->buildWhere([$index], $condition, $params); + + return $where === '' ? $sql : $sql . ' ' . $where; + } + + /** + * Builds a SQL statement for truncating an index. + * @param string $index the index to be truncated. The name will be properly quoted by the method. + * @return string the SQL statement for truncating an index. + */ + public function truncateIndex($index) + { + return 'TRUNCATE RTINDEX ' . $this->db->quoteIndexName($index); + } + + /** + * Builds a SQL statement for call snippet from provided data and query, using specified index settings. + * @param string $index name of the index, from which to take the text processing settings. + * @param string|array $source is the source data to extract a snippet from. + * It could be either a single string or array of strings. + * @param string $match the full-text query to build snippets for. + * @param array $options list of options in format: optionName => optionValue + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the Sphinx command later. + * @return string the SQL statement for call snippets. + */ + public function callSnippets($index, $source, $match, $options, &$params) + { + if (is_array($source)) { + $dataSqlParts = []; + foreach ($source as $sourceRow) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $sourceRow; + $dataSqlParts[] = $phName; + } + $dataSql = '(' . implode(',', $dataSqlParts) . ')'; + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $source; + $dataSql = $phName; + } + $indexParamName = self::PARAM_PREFIX . count($params); + $params[$indexParamName] = $index; + $matchParamName = self::PARAM_PREFIX . count($params); + $params[$matchParamName] = $match; + if (!empty($options)) { + $optionParts = []; + foreach ($options as $name => $value) { + if ($value instanceof Expression) { + $actualValue = $value->expression; + } else { + $actualValue = self::PARAM_PREFIX . count($params); + $params[$actualValue] = $value; + } + $optionParts[] = $actualValue . ' AS ' . $name; + } + $optionSql = ', ' . implode(', ', $optionParts); + } else { + $optionSql = ''; + } + + return 'CALL SNIPPETS(' . $dataSql. ', ' . $indexParamName . ', ' . $matchParamName . $optionSql. ')'; + } + + /** + * Builds a SQL statement for returning tokenized and normalized forms of the keywords, and, + * optionally, keyword statistics. + * @param string $index the name of the index from which to take the text processing settings + * @param string $text the text to break down to keywords. + * @param boolean $fetchStatistic whether to return document and hit occurrence statistics + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the Sphinx command later. + * @return string the SQL statement for call keywords. + */ + public function callKeywords($index, $text, $fetchStatistic, &$params) + { + $indexParamName = self::PARAM_PREFIX . count($params); + $params[$indexParamName] = $index; + $textParamName = self::PARAM_PREFIX . count($params); + $params[$textParamName] = $text; + + return 'CALL KEYWORDS(' . $textParamName . ', ' . $indexParamName . ($fetchStatistic ? ', 1' : '') . ')'; + } + + /** + * @param array $columns + * @param array $params the binding parameters to be populated + * @param boolean $distinct + * @param string $selectOption + * @return string the SELECT clause built from [[query]]. + */ + public function buildSelect($columns, &$params, $distinct = false, $selectOption = null) + { + $select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; + if ($selectOption !== null) { + $select .= ' ' . $selectOption; + } + + if (empty($columns)) { + return $select . ' *'; + } + + foreach ($columns as $i => $column) { + if ($column instanceof Expression) { + $columns[$i] = $column->expression; + $params = array_merge($params, $column->params); + } elseif (is_string($i)) { + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $columns[$i] = "$column AS " . $this->db->quoteColumnName($i); + } elseif (strpos($column, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { + $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); + } else { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + } + + return $select . ' ' . implode(', ', $columns); + } + + /** + * @param array $indexes + * @param array $params the binding parameters to be populated + * @return string the FROM clause built from [[query]]. + */ + public function buildFrom($indexes, &$params) + { + if (empty($indexes)) { + return ''; + } + + foreach ($indexes as $i => $index) { + if ($index instanceof Query) { + list($sql, $params) = $this->build($index, $params); + $indexes[$i] = "($sql) " . $this->db->quoteIndexName($i); + } elseif (is_string($i)) { + if (strpos($index, '(') === false) { + $index = $this->db->quoteIndexName($index); + } + $indexes[$i] = "$index " . $this->db->quoteIndexName($i); + } elseif (strpos($index, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $index, $matches)) { // with alias + $indexes[$i] = $this->db->quoteIndexName($matches[1]) . ' ' . $this->db->quoteIndexName($matches[2]); + } else { + $indexes[$i] = $this->db->quoteIndexName($index); + } + } + } + + if (is_array($indexes)) { + $indexes = implode(', ', $indexes); + } + + return 'FROM ' . $indexes; + } + + /** + * @param string[] $indexes list of index names, which affected by query + * @param string|array $condition + * @param array $params the binding parameters to be populated + * @return string the WHERE clause built from [[query]]. + */ + public function buildWhere($indexes, $condition, &$params) + { + if (empty($condition)) { + return ''; + } + $indexSchemas = []; + if (!empty($indexes)) { + foreach ($indexes as $indexName) { + $index = $this->db->getIndexSchema($indexName); + if ($index !== null) { + $indexSchemas[] = $index; + } + } + } + $where = $this->buildCondition($indexSchemas, $condition, $params); + + return $where === '' ? '' : 'WHERE ' . $where; + } + + /** + * @param array $columns + * @return string the GROUP BY clause + */ + public function buildGroupBy($columns) + { + return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); + } + + /** + * @param array $columns + * @return string the ORDER BY clause built from [[query]]. + */ + public function buildOrderBy($columns) + { + if (empty($columns)) { + return ''; + } + $orders = []; + foreach ($columns as $name => $direction) { + if ($direction instanceof Expression) { + $orders[] = $direction->expression; + } else { + $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : 'ASC'); + } + } + + return 'ORDER BY ' . implode(', ', $orders); + } + + /** + * @param integer $limit + * @param integer $offset + * @return string the LIMIT and OFFSET clauses built from [[query]]. + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + if (is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0') { + $sql = 'LIMIT ' . $offset; + } + if (is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0) { + $sql = $sql === '' ? "LIMIT $limit" : "$sql,$limit"; + } elseif ($sql !== '') { + $sql .= ',1000'; // this is the default limit by sphinx + } + + return $sql; + } + + /** + * Processes columns and properly quote them if necessary. + * It will join all columns into a string with comma as separators. + * @param string|array $columns the columns to be processed + * @return string the processing result + */ + public function buildColumns($columns) + { + if (!is_array($columns)) { + if (strpos($columns, '(') !== false) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + } + } + foreach ($columns as $i => $column) { + if ($column instanceof Expression) { + $columns[$i] = $column->expression; + } elseif (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + + return is_array($columns) ? implode(', ', $columns) : $columns; + } + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition($indexes, $condition, &$params) + { + static $builders = [ + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', + ]; + + if (!is_array($condition)) { + return (string) $condition; + } elseif (empty($condition)) { + return ''; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + + return $this->$method($indexes, $operator, $condition, $params); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + + return $this->buildHashCondition($indexes, $condition, $params); + } + } + + /** + * Creates a condition based on column-value pairs. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param array $condition the condition specification. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildHashCondition($indexes, $condition, &$params) + { + $parts = []; + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition($indexes, 'IN', [$column, $value], $params); + } else { + if (strpos($column, '(') === false) { + $quotedColumn = $this->db->quoteColumnName($column); + } else { + $quotedColumn = $column; + } + if ($value === null) { + $parts[] = "$quotedColumn IS NULL"; + } else { + $parts[] = $quotedColumn . '=' . $this->composeColumnValue($indexes, $column, $value, $params); + } + } + } + + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + /** + * Connects two or more SQL expressions with the `AND` or `OR` operator. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the SQL expressions to connect. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildAndCondition($indexes, $operator, $operands, &$params) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($indexes, $operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + /** + * Creates an SQL expressions with the `BETWEEN` operator. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) + * @param array $operands the first operand is the column name. The second and third operands + * describe the interval that column value should be in. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws Exception if wrong number of operands have been given. + */ + public function buildBetweenCondition($indexes, $operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + if (strpos($column, '(') === false) { + $quotedColumn = $this->db->quoteColumnName($column); + } else { + $quotedColumn = $column; + } + $phName1 = $this->composeColumnValue($indexes, $column, $value1, $params); + $phName2 = $this->composeColumnValue($indexes, $column, $value2, $params); + + return "$quotedColumn $operator $phName1 AND $phName2"; + } + + /** + * Creates an SQL expressions with the `IN` operator. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) + * @param array $operands the first operand is the column name. If it is an array + * a composite IN condition will be generated. + * The second operand is an array of values that column value should be among. + * If it is an empty array the generated expression will be a `false` value if + * operator is `IN` and empty if operator is `NOT IN`. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws Exception if wrong number of operands have been given. + */ + public function buildInCondition($indexes, $operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array) $values; + + if (empty($values) || $column === []) { + return $operator === 'IN' ? '0=1' : ''; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($indexes, $operator, $column, $values, $params); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + $values[$i] = $this->composeColumnValue($indexes, $column, $value, $params); + } + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + + return $column . $operator . reset($values); + } + } + + /** + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) + * @param array $columns + * @param array $values + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + protected function buildCompositeInCondition($indexes, $operator, $columns, $values, &$params) + { + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($columns as $column) { + if (isset($value[$column])) { + $vs[] = $this->composeColumnValue($indexes, $column, $value[$column], $params); + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + /** + * Creates an SQL expressions with the `LIKE` operator. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) + * @param array $operands an array of two or three operands + * + * - The first operand is the column name. + * - The second operand is a single value or an array of values that column value + * should be compared with. If it is an empty array the generated expression will + * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator + * is `NOT LIKE` or `OR NOT LIKE`. + * - An optional third operand can also be provided to specify how to escape special characters + * in the value(s). The operand should be an array of mappings from the special characters to their + * escaped counterparts. If this operand is not provided, a default escape mapping will be used. + * You may use `false` or an empty array to indicate the values are already escaped and no escape + * should be applied. Note that when using an escape mapping (or the third operand is not provided), + * the values will be automatically enclosed within a pair of percentage characters. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildLikeCondition($indexes, $operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']; + unset($operands[2]); + + list($column, $values) = $operands; + + $values = (array) $values; + + if (empty($values)) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = []; + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } + + /** + * @param array $columns + * @return string the ORDER BY clause built from [[query]]. + */ + public function buildWithin($columns) + { + if (empty($columns)) { + return ''; + } + $orders = []; + foreach ($columns as $name => $direction) { + if ($direction instanceof Expression) { + $orders[] = $direction->expression; + } else { + $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); + } + } + + return 'WITHIN GROUP ORDER BY ' . implode(', ', $orders); + } + + /** + * @param array $options query options in format: optionName => optionValue + * @param array $params the binding parameters to be populated + * @return string the OPTION clause build from [[query]] + */ + public function buildOption($options, &$params) + { + if (empty($options)) { + return ''; + } + $optionLines = []; + foreach ($options as $name => $value) { + if ($value instanceof Expression) { + $actualValue = $value->expression; + } else { + if (is_array($value)) { + $actualValueParts = []; + foreach ($value as $key => $valuePart) { + if (is_numeric($key)) { + $actualValuePart = ''; + } else { + $actualValuePart = $key . ' = '; + } + if ($valuePart instanceof Expression) { + $actualValuePart .= $valuePart->expression; + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $valuePart; + $actualValuePart .= $phName; + } + $actualValueParts[] = $actualValuePart; + } + $actualValue = '(' . implode(', ', $actualValueParts) . ')'; + } else { + $actualValue = self::PARAM_PREFIX . count($params); + $params[$actualValue] = $value; + } + } + $optionLines[] = $name . ' = ' . $actualValue; + } + + return 'OPTION ' . implode(', ', $optionLines); + } + + /** + * Composes column value for SQL, taking in account the column type. + * @param IndexSchema[] $indexes list of indexes, which affected by query + * @param string $columnName name of the column + * @param mixed $value raw column value + * @param array $params the binding parameters to be populated + * @return string SQL expression, which represents column value + */ + protected function composeColumnValue($indexes, $columnName, $value, &$params) + { + if ($value === null) { + return 'NULL'; + } elseif ($value instanceof Expression) { + $params = array_merge($params, $value->params); + + return $value->expression; + } + foreach ($indexes as $index) { + $columnSchema = $index->getColumn($columnName); + if ($columnSchema !== null) { + break; + } + } + if (is_array($value)) { + // MVA : + $lineParts = []; + foreach ($value as $subValue) { + if ($subValue instanceof Expression) { + $params = array_merge($params, $subValue->params); + $lineParts[] = $subValue->expression; + } else { + $phName = self::PARAM_PREFIX . count($params); + $lineParts[] = $phName; + $params[$phName] = (isset($columnSchema)) ? $columnSchema->typecast($subValue) : $subValue; + } + } + + return '(' . implode(',', $lineParts) . ')'; + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = (isset($columnSchema)) ? $columnSchema->typecast($value) : $value; + + return $phName; + } + } } diff --git a/extensions/sphinx/Schema.php b/extensions/sphinx/Schema.php index 81a7a57e383..f1b20fa61d5 100644 --- a/extensions/sphinx/Schema.php +++ b/extensions/sphinx/Schema.php @@ -27,463 +27,476 @@ */ class Schema extends Object { - /** - * The followings are the supported abstract column data types. - */ - const TYPE_PK = 'pk'; - const TYPE_STRING = 'string'; - const TYPE_INTEGER = 'integer'; - const TYPE_BIGINT = 'bigint'; - const TYPE_FLOAT = 'float'; - const TYPE_TIMESTAMP = 'timestamp'; - const TYPE_BOOLEAN = 'boolean'; - - /** - * @var Connection the Sphinx connection - */ - public $db; - /** - * @var array list of ALL index names in the Sphinx - */ - private $_indexNames; - /** - * @var array list of ALL index types in the Sphinx (index name => index type) - */ - private $_indexTypes; - /** - * @var array list of loaded index metadata (index name => IndexSchema) - */ - private $_indexes = []; - /** - * @var QueryBuilder the query builder for this Sphinx connection - */ - private $_builder; - - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - */ - public $typeMap = [ - 'field' => self::TYPE_STRING, - 'string' => self::TYPE_STRING, - 'ordinal' => self::TYPE_STRING, - 'integer' => self::TYPE_INTEGER, - 'int' => self::TYPE_INTEGER, - 'uint' => self::TYPE_INTEGER, - 'bigint' => self::TYPE_BIGINT, - 'timestamp' => self::TYPE_TIMESTAMP, - 'bool' => self::TYPE_BOOLEAN, - 'float' => self::TYPE_FLOAT, - 'mva' => self::TYPE_INTEGER, - ]; - - /** - * Loads the metadata for the specified index. - * @param string $name index name - * @return IndexSchema driver dependent index metadata. Null if the index does not exist. - */ - protected function loadIndexSchema($name) - { - $index = new IndexSchema; - $this->resolveIndexNames($index, $name); - $this->resolveIndexType($index); - - if ($this->findColumns($index)) { - return $index; - } else { - return null; - } - } - - /** - * Resolves the index name. - * @param IndexSchema $index the index metadata object - * @param string $name the index name - */ - protected function resolveIndexNames($index, $name) - { - $index->name = str_replace('`', '', $name); - } - - /** - * Resolves the index name. - * @param IndexSchema $index the index metadata object - */ - protected function resolveIndexType($index) - { - $indexTypes = $this->getIndexTypes(); - $index->type = array_key_exists($index->name, $indexTypes) ? $indexTypes[$index->name] : 'unknown'; - $index->isRuntime = ($index->type == 'rt'); - } - - /** - * Obtains the metadata for the named index. - * @param string $name index name. The index name may contain schema name if any. Do not quote the index name. - * @param boolean $refresh whether to reload the index schema even if it is found in the cache. - * @return IndexSchema index metadata. Null if the named index does not exist. - */ - public function getIndexSchema($name, $refresh = false) - { - if (isset($this->_indexes[$name]) && !$refresh) { - return $this->_indexes[$name]; - } - - $db = $this->db; - $realName = $this->getRawIndexName($name); - - if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { - /** @var $cache Cache */ - $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; - if ($cache instanceof Cache) { - $key = $this->getCacheKey($name); - if ($refresh || ($index = $cache->get($key)) === false) { - $index = $this->loadIndexSchema($realName); - if ($index !== null) { - $cache->set($key, $index, $db->schemaCacheDuration, new GroupDependency([ - 'group' => $this->getCacheGroup(), - ])); - } - } - return $this->_indexes[$name] = $index; - } - } - return $this->_indexes[$name] = $this->loadIndexSchema($realName); - } - - /** - * Returns the cache key for the specified index name. - * @param string $name the index name - * @return mixed the cache key - */ - protected function getCacheKey($name) - { - return [ - __CLASS__, - $this->db->dsn, - $this->db->username, - $name, - ]; - } - - /** - * Returns the cache group name. - * This allows [[refresh()]] to invalidate all cached index schemas. - * @return string the cache group name - */ - protected function getCacheGroup() - { - return md5(serialize([ - __CLASS__, - $this->db->dsn, - $this->db->username, - ])); - } - - /** - * Returns the metadata for all indexes in the database. - * @param boolean $refresh whether to fetch the latest available index schemas. If this is false, - * cached data may be returned if available. - * @return IndexSchema[] the metadata for all indexes in the Sphinx. - * Each array element is an instance of [[IndexSchema]] or its child class. - */ - public function getIndexSchemas($refresh = false) - { - $indexes = []; - foreach ($this->getIndexNames($refresh) as $name) { - if (($index = $this->getIndexSchema($name, $refresh)) !== null) { - $indexes[] = $index; - } - } - return $indexes; - } - - /** - * Returns all index names in the Sphinx. - * @param boolean $refresh whether to fetch the latest available index names. If this is false, - * index names fetched previously (if available) will be returned. - * @return string[] all index names in the Sphinx. - */ - public function getIndexNames($refresh = false) - { - if (!isset($this->_indexNames) || $refresh) { - $this->initIndexesInfo(); - } - return $this->_indexNames; - } - - /** - * Returns all index types in the Sphinx. - * @param boolean $refresh whether to fetch the latest available index types. If this is false, - * index types fetched previously (if available) will be returned. - * @return array all index types in the Sphinx in format: index name => index type. - */ - public function getIndexTypes($refresh = false) - { - if (!isset($this->_indexTypes) || $refresh) { - $this->initIndexesInfo(); - } - return $this->_indexTypes; - } - - /** - * Initializes information about name and type of all index in the Sphinx. - */ - protected function initIndexesInfo() - { - $this->_indexNames = []; - $this->_indexTypes = []; - $indexes = $this->findIndexes(); - foreach ($indexes as $index) { - $indexName = $index['Index']; - $this->_indexNames[] = $indexName; - $this->_indexTypes[$indexName] = $index['Type']; - } - } - - /** - * Returns all index names in the Sphinx. - * @return array all index names in the Sphinx. - */ - protected function findIndexes() - { - $sql = 'SHOW TABLES'; - return $this->db->createCommand($sql)->queryAll(); - } - - /** - * @return QueryBuilder the query builder for this connection. - */ - public function getQueryBuilder() - { - if ($this->_builder === null) { - $this->_builder = $this->createQueryBuilder(); - } - return $this->_builder; - } - - /** - * Determines the PDO type for the given PHP data value. - * @param mixed $data the data whose PDO type is to be determined - * @return integer the PDO type - * @see http://www.php.net/manual/en/pdo.constants.php - */ - public function getPdoType($data) - { - static $typeMap = [ - // php type => PDO type - 'boolean' => \PDO::PARAM_BOOL, - 'integer' => \PDO::PARAM_INT, - 'string' => \PDO::PARAM_STR, - 'resource' => \PDO::PARAM_LOB, - 'NULL' => \PDO::PARAM_NULL, - ]; - $type = gettype($data); - return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; - } - - /** - * Refreshes the schema. - * This method cleans up all cached index schemas so that they can be re-created later - * to reflect the Sphinx schema change. - */ - public function refresh() - { - /** @var $cache Cache */ - $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; - if ($this->db->enableSchemaCache && $cache instanceof Cache) { - GroupDependency::invalidate($cache, $this->getCacheGroup()); - } - $this->_indexNames = []; - $this->_indexes = []; - } - - /** - * Creates a query builder for the Sphinx. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Quotes a string value for use in a query. - * Note that if the parameter is not a string, it will be returned without change. - * @param string $str string to be quoted - * @return string the properly quoted string - * @see http://www.php.net/manual/en/function.PDO-quote.php - */ - public function quoteValue($str) - { - if (!is_string($str)) { - return $str; - } - $this->db->open(); - return $this->db->pdo->quote($str); - } - - /** - * Quotes a index name for use in a query. - * If the index name contains schema prefix, the prefix will also be properly quoted. - * If the index name is already quoted or contains '(' or '{{', - * then this method will do nothing. - * @param string $name index name - * @return string the properly quoted index name - * @see quoteSimpleTableName - */ - public function quoteIndexName($name) - { - if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { - return $name; - } - return $this->quoteSimpleIndexName($name); - } - - /** - * Quotes a column name for use in a query. - * If the column name contains prefix, the prefix will also be properly quoted. - * If the column name is already quoted or contains '(', '[[' or '{{', - * then this method will do nothing. - * @param string $name column name - * @return string the properly quoted column name - * @see quoteSimpleColumnName - */ - public function quoteColumnName($name) - { - if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { - return $name; - } - if (($pos = strrpos($name, '.')) !== false) { - $prefix = $this->quoteIndexName(substr($name, 0, $pos)) . '.'; - $name = substr($name, $pos + 1); - } else { - $prefix = ''; - } - return $prefix . $this->quoteSimpleColumnName($name); - } - - /** - * Quotes a index name for use in a query. - * A simple index name has no schema prefix. - * @param string $name index name - * @return string the properly quoted index name - */ - public function quoteSimpleIndexName($name) - { - return strpos($name, "`") !== false ? $name : "`" . $name . "`"; - } - - /** - * Quotes a column name for use in a query. - * A simple column name has no prefix. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; - } - - /** - * Returns the actual name of a given index name. - * This method will strip off curly brackets from the given index name - * and replace the percentage character '%' with [[Connection::indexPrefix]]. - * @param string $name the index name to be converted - * @return string the real name of the given index name - */ - public function getRawIndexName($name) - { - if (strpos($name, '{{') !== false) { - $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); - return str_replace('%', $this->db->tablePrefix, $name); - } else { - return $name; - } - } - - /** - * Extracts the PHP type from abstract DB type. - * @param ColumnSchema $column the column schema information - * @return string PHP type name - */ - protected function getColumnPhpType($column) - { - static $typeMap = [ // abstract type => php type - 'smallint' => 'integer', - 'integer' => 'integer', - 'bigint' => 'integer', - 'boolean' => 'boolean', - 'float' => 'double', - ]; - if (isset($typeMap[$column->type])) { - if ($column->type === 'bigint') { - return PHP_INT_SIZE == 8 ? 'integer' : 'string'; - } elseif ($column->type === 'integer') { - return PHP_INT_SIZE == 4 ? 'string' : 'integer'; - } else { - return $typeMap[$column->type]; - } - } else { - return 'string'; - } - } - - /** - * Collects the metadata of index columns. - * @param IndexSchema $index the index metadata - * @return boolean whether the index exists in the database - * @throws \Exception if DB query fails - */ - protected function findColumns($index) - { - $sql = 'DESCRIBE ' . $this->quoteSimpleIndexName($index->name); - try { - $columns = $this->db->createCommand($sql)->queryAll(); - } catch (\Exception $e) { - $previous = $e->getPrevious(); - if ($previous instanceof \PDOException && $previous->getCode() == '42S02') { - // index does not exist - return false; - } - throw $e; - } - foreach ($columns as $info) { - $column = $this->loadColumnSchema($info); - $index->columns[$column->name] = $column; - if ($column->isPrimaryKey) { - $index->primaryKey = $column->name; - } - } - return true; - } - - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema; - - $column->name = $info['Field']; - $column->dbType = $info['Type']; - - $column->isPrimaryKey = ($column->name == 'id'); - - $type = $info['Type']; - if (isset($this->typeMap[$type])) { - $column->type = $this->typeMap[$type]; - } else { - $column->type = self::TYPE_STRING; - } - - $column->isField = ($type == 'field'); - $column->isAttribute = !$column->isField; - - $column->isMva = ($type == 'mva'); - - $column->phpType = $this->getColumnPhpType($column); - - return $column; - } + /** + * The followings are the supported abstract column data types. + */ + const TYPE_PK = 'pk'; + const TYPE_STRING = 'string'; + const TYPE_INTEGER = 'integer'; + const TYPE_BIGINT = 'bigint'; + const TYPE_FLOAT = 'float'; + const TYPE_TIMESTAMP = 'timestamp'; + const TYPE_BOOLEAN = 'boolean'; + + /** + * @var Connection the Sphinx connection + */ + public $db; + /** + * @var array list of ALL index names in the Sphinx + */ + private $_indexNames; + /** + * @var array list of ALL index types in the Sphinx (index name => index type) + */ + private $_indexTypes; + /** + * @var array list of loaded index metadata (index name => IndexSchema) + */ + private $_indexes = []; + /** + * @var QueryBuilder the query builder for this Sphinx connection + */ + private $_builder; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = [ + 'field' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + 'ordinal' => self::TYPE_STRING, + 'integer' => self::TYPE_INTEGER, + 'int' => self::TYPE_INTEGER, + 'uint' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'timestamp' => self::TYPE_TIMESTAMP, + 'bool' => self::TYPE_BOOLEAN, + 'float' => self::TYPE_FLOAT, + 'mva' => self::TYPE_INTEGER, + ]; + + /** + * Loads the metadata for the specified index. + * @param string $name index name + * @return IndexSchema driver dependent index metadata. Null if the index does not exist. + */ + protected function loadIndexSchema($name) + { + $index = new IndexSchema; + $this->resolveIndexNames($index, $name); + $this->resolveIndexType($index); + + if ($this->findColumns($index)) { + return $index; + } else { + return null; + } + } + + /** + * Resolves the index name. + * @param IndexSchema $index the index metadata object + * @param string $name the index name + */ + protected function resolveIndexNames($index, $name) + { + $index->name = str_replace('`', '', $name); + } + + /** + * Resolves the index name. + * @param IndexSchema $index the index metadata object + */ + protected function resolveIndexType($index) + { + $indexTypes = $this->getIndexTypes(); + $index->type = array_key_exists($index->name, $indexTypes) ? $indexTypes[$index->name] : 'unknown'; + $index->isRuntime = ($index->type == 'rt'); + } + + /** + * Obtains the metadata for the named index. + * @param string $name index name. The index name may contain schema name if any. Do not quote the index name. + * @param boolean $refresh whether to reload the index schema even if it is found in the cache. + * @return IndexSchema index metadata. Null if the named index does not exist. + */ + public function getIndexSchema($name, $refresh = false) + { + if (isset($this->_indexes[$name]) && !$refresh) { + return $this->_indexes[$name]; + } + + $db = $this->db; + $realName = $this->getRawIndexName($name); + + if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { + /** @var $cache Cache */ + $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; + if ($cache instanceof Cache) { + $key = $this->getCacheKey($name); + if ($refresh || ($index = $cache->get($key)) === false) { + $index = $this->loadIndexSchema($realName); + if ($index !== null) { + $cache->set($key, $index, $db->schemaCacheDuration, new GroupDependency([ + 'group' => $this->getCacheGroup(), + ])); + } + } + + return $this->_indexes[$name] = $index; + } + } + + return $this->_indexes[$name] = $this->loadIndexSchema($realName); + } + + /** + * Returns the cache key for the specified index name. + * @param string $name the index name + * @return mixed the cache key + */ + protected function getCacheKey($name) + { + return [ + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + ]; + } + + /** + * Returns the cache group name. + * This allows [[refresh()]] to invalidate all cached index schemas. + * @return string the cache group name + */ + protected function getCacheGroup() + { + return md5(serialize([ + __CLASS__, + $this->db->dsn, + $this->db->username, + ])); + } + + /** + * Returns the metadata for all indexes in the database. + * @param boolean $refresh whether to fetch the latest available index schemas. If this is false, + * cached data may be returned if available. + * @return IndexSchema[] the metadata for all indexes in the Sphinx. + * Each array element is an instance of [[IndexSchema]] or its child class. + */ + public function getIndexSchemas($refresh = false) + { + $indexes = []; + foreach ($this->getIndexNames($refresh) as $name) { + if (($index = $this->getIndexSchema($name, $refresh)) !== null) { + $indexes[] = $index; + } + } + + return $indexes; + } + + /** + * Returns all index names in the Sphinx. + * @param boolean $refresh whether to fetch the latest available index names. If this is false, + * index names fetched previously (if available) will be returned. + * @return string[] all index names in the Sphinx. + */ + public function getIndexNames($refresh = false) + { + if (!isset($this->_indexNames) || $refresh) { + $this->initIndexesInfo(); + } + + return $this->_indexNames; + } + + /** + * Returns all index types in the Sphinx. + * @param boolean $refresh whether to fetch the latest available index types. If this is false, + * index types fetched previously (if available) will be returned. + * @return array all index types in the Sphinx in format: index name => index type. + */ + public function getIndexTypes($refresh = false) + { + if (!isset($this->_indexTypes) || $refresh) { + $this->initIndexesInfo(); + } + + return $this->_indexTypes; + } + + /** + * Initializes information about name and type of all index in the Sphinx. + */ + protected function initIndexesInfo() + { + $this->_indexNames = []; + $this->_indexTypes = []; + $indexes = $this->findIndexes(); + foreach ($indexes as $index) { + $indexName = $index['Index']; + $this->_indexNames[] = $indexName; + $this->_indexTypes[$indexName] = $index['Type']; + } + } + + /** + * Returns all index names in the Sphinx. + * @return array all index names in the Sphinx. + */ + protected function findIndexes() + { + $sql = 'SHOW TABLES'; + + return $this->db->createCommand($sql)->queryAll(); + } + + /** + * @return QueryBuilder the query builder for this connection. + */ + public function getQueryBuilder() + { + if ($this->_builder === null) { + $this->_builder = $this->createQueryBuilder(); + } + + return $this->_builder; + } + + /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + static $typeMap = [ + // php type => PDO type + 'boolean' => \PDO::PARAM_BOOL, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** + * Refreshes the schema. + * This method cleans up all cached index schemas so that they can be re-created later + * to reflect the Sphinx schema change. + */ + public function refresh() + { + /** @var $cache Cache */ + $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; + if ($this->db->enableSchemaCache && $cache instanceof Cache) { + GroupDependency::invalidate($cache, $this->getCacheGroup()); + } + $this->_indexNames = []; + $this->_indexes = []; + } + + /** + * Creates a query builder for the Sphinx. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Quotes a string value for use in a query. + * Note that if the parameter is not a string, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + if (!is_string($str)) { + return $str; + } + $this->db->open(); + + return $this->db->pdo->quote($str); + } + + /** + * Quotes a index name for use in a query. + * If the index name contains schema prefix, the prefix will also be properly quoted. + * If the index name is already quoted or contains '(' or '{{', + * then this method will do nothing. + * @param string $name index name + * @return string the properly quoted index name + * @see quoteSimpleTableName + */ + public function quoteIndexName($name) + { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { + return $name; + } + + return $this->quoteSimpleIndexName($name); + } + + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * If the column name is already quoted or contains '(', '[[' or '{{', + * then this method will do nothing. + * @param string $name column name + * @return string the properly quoted column name + * @see quoteSimpleColumnName + */ + public function quoteColumnName($name) + { + if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (($pos = strrpos($name, '.')) !== false) { + $prefix = $this->quoteIndexName(substr($name, 0, $pos)) . '.'; + $name = substr($name, $pos + 1); + } else { + $prefix = ''; + } + + return $prefix . $this->quoteSimpleColumnName($name); + } + + /** + * Quotes a index name for use in a query. + * A simple index name has no schema prefix. + * @param string $name index name + * @return string the properly quoted index name + */ + public function quoteSimpleIndexName($name) + { + return strpos($name, "`") !== false ? $name : "`" . $name . "`"; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; + } + + /** + * Returns the actual name of a given index name. + * This method will strip off curly brackets from the given index name + * and replace the percentage character '%' with [[Connection::indexPrefix]]. + * @param string $name the index name to be converted + * @return string the real name of the given index name + */ + public function getRawIndexName($name) + { + if (strpos($name, '{{') !== false) { + $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); + + return str_replace('%', $this->db->tablePrefix, $name); + } else { + return $name; + } + } + + /** + * Extracts the PHP type from abstract DB type. + * @param ColumnSchema $column the column schema information + * @return string PHP type name + */ + protected function getColumnPhpType($column) + { + static $typeMap = [ // abstract type => php type + 'smallint' => 'integer', + 'integer' => 'integer', + 'bigint' => 'integer', + 'boolean' => 'boolean', + 'float' => 'double', + ]; + if (isset($typeMap[$column->type])) { + if ($column->type === 'bigint') { + return PHP_INT_SIZE == 8 ? 'integer' : 'string'; + } elseif ($column->type === 'integer') { + return PHP_INT_SIZE == 4 ? 'string' : 'integer'; + } else { + return $typeMap[$column->type]; + } + } else { + return 'string'; + } + } + + /** + * Collects the metadata of index columns. + * @param IndexSchema $index the index metadata + * @return boolean whether the index exists in the database + * @throws \Exception if DB query fails + */ + protected function findColumns($index) + { + $sql = 'DESCRIBE ' . $this->quoteSimpleIndexName($index->name); + try { + $columns = $this->db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + $previous = $e->getPrevious(); + if ($previous instanceof \PDOException && $previous->getCode() == '42S02') { + // index does not exist + return false; + } + throw $e; + } + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); + $index->columns[$column->name] = $column; + if ($column->isPrimaryKey) { + $index->primaryKey = $column->name; + } + } + + return true; + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema; + + $column->name = $info['Field']; + $column->dbType = $info['Type']; + + $column->isPrimaryKey = ($column->name == 'id'); + + $type = $info['Type']; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } else { + $column->type = self::TYPE_STRING; + } + + $column->isField = ($type == 'field'); + $column->isAttribute = !$column->isField; + + $column->isMva = ($type == 'mva'); + + $column->phpType = $this->getColumnPhpType($column); + + return $column; + } } diff --git a/extensions/swiftmailer/Mailer.php b/extensions/swiftmailer/Mailer.php index 8a87ba251ef..1f91f6d86cd 100644 --- a/extensions/swiftmailer/Mailer.php +++ b/extensions/swiftmailer/Mailer.php @@ -75,146 +75,151 @@ */ class Mailer extends BaseMailer { - /** - * @var string message default class name. - */ - public $messageClass = 'yii\swiftmailer\Message'; - /** - * @var \Swift_Mailer Swift mailer instance. - */ - private $_swiftMailer; - /** - * @var \Swift_Transport|array Swift transport instance or its array configuration. - */ - private $_transport = []; + /** + * @var string message default class name. + */ + public $messageClass = 'yii\swiftmailer\Message'; + /** + * @var \Swift_Mailer Swift mailer instance. + */ + private $_swiftMailer; + /** + * @var \Swift_Transport|array Swift transport instance or its array configuration. + */ + private $_transport = []; - /** - * @return array|\Swift_Mailer Swift mailer instance or array configuration. - */ - public function getSwiftMailer() - { - if (!is_object($this->_swiftMailer)) { - $this->_swiftMailer = $this->createSwiftMailer(); - } - return $this->_swiftMailer; - } + /** + * @return array|\Swift_Mailer Swift mailer instance or array configuration. + */ + public function getSwiftMailer() + { + if (!is_object($this->_swiftMailer)) { + $this->_swiftMailer = $this->createSwiftMailer(); + } - /** - * @param array|\Swift_Transport $transport - * @throws InvalidConfigException on invalid argument. - */ - public function setTransport($transport) - { - if (!is_array($transport) && !is_object($transport)) { - throw new InvalidConfigException('"' . get_class($this) . '::transport" should be either object or array, "' . gettype($transport) . '" given.'); - } - $this->_transport = $transport; - } + return $this->_swiftMailer; + } - /** - * @return array|\Swift_Transport - */ - public function getTransport() - { - if (!is_object($this->_transport)) { - $this->_transport = $this->createTransport($this->_transport); - } - return $this->_transport; - } + /** + * @param array|\Swift_Transport $transport + * @throws InvalidConfigException on invalid argument. + */ + public function setTransport($transport) + { + if (!is_array($transport) && !is_object($transport)) { + throw new InvalidConfigException('"' . get_class($this) . '::transport" should be either object or array, "' . gettype($transport) . '" given.'); + } + $this->_transport = $transport; + } - /** - * @inheritdoc - */ - protected function sendMessage($message) - { - $address = $message->getTo(); - if (is_array($address)) { - $address = implode(', ', array_keys($address)); - } - Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__); - return $this->getSwiftMailer()->send($message->getSwiftMessage()) > 0; - } + /** + * @return array|\Swift_Transport + */ + public function getTransport() + { + if (!is_object($this->_transport)) { + $this->_transport = $this->createTransport($this->_transport); + } - /** - * Creates Swift mailer instance. - * @return \Swift_Mailer mailer instance. - */ - protected function createSwiftMailer() - { - return \Swift_Mailer::newInstance($this->getTransport()); - } + return $this->_transport; + } - /** - * Creates email transport instance by its array configuration. - * @param array $config transport configuration. - * @throws \yii\base\InvalidConfigException on invalid transport configuration. - * @return \Swift_Transport transport instance. - */ - protected function createTransport(array $config) - { - if (!isset($config['class'])) { - $config['class'] = 'Swift_MailTransport'; - } - if (isset($config['plugins'])) { - $plugins = $config['plugins']; - unset($config['plugins']); - } - /** @var \Swift_MailTransport $transport */ - $transport = $this->createSwiftObject($config); - if (isset($plugins)) { - foreach ($plugins as $plugin) { - if (is_array($plugin) && isset($plugin['class'])) { - $plugin = $this->createSwiftObject($plugin); - } - $transport->registerPlugin($plugin); - } - } - return $transport; - } + /** + * @inheritdoc + */ + protected function sendMessage($message) + { + $address = $message->getTo(); + if (is_array($address)) { + $address = implode(', ', array_keys($address)); + } + Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__); - /** - * Creates Swift library object, from given array configuration. - * @param array $config object configuration - * @return Object created object - * @throws \yii\base\InvalidConfigException on invalid configuration. - */ - protected function createSwiftObject(array $config) - { - if (isset($config['class'])) { - $className = $config['class']; - unset($config['class']); - } else { - throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); - } - if (isset($config['constructArgs'])) { - $args = []; - foreach ($config['constructArgs'] as $arg) { - if (is_array($arg) && isset($arg['class'])) { - $args[] = $this->createSwiftObject($arg); - } else { - $args[] = $arg; - } - } - unset($config['constructArgs']); - array_unshift($args, $className); - $object = call_user_func_array(['Yii', 'createObject'], $args); - } else { - $object = new $className; - } - if (!empty($config)) { - foreach ($config as $name => $value) { - if (property_exists($object, $name)) { - $object->$name = $value; - } else { - $setter = 'set' . $name; - if (method_exists($object, $setter) || method_exists($object, '__call')) { - $object->$setter($value); - } else { - throw new InvalidConfigException('Setting unknown property: ' . $className . '::' . $name); - } - } - } - } - return $object; - } + return $this->getSwiftMailer()->send($message->getSwiftMessage()) > 0; + } + + /** + * Creates Swift mailer instance. + * @return \Swift_Mailer mailer instance. + */ + protected function createSwiftMailer() + { + return \Swift_Mailer::newInstance($this->getTransport()); + } + + /** + * Creates email transport instance by its array configuration. + * @param array $config transport configuration. + * @throws \yii\base\InvalidConfigException on invalid transport configuration. + * @return \Swift_Transport transport instance. + */ + protected function createTransport(array $config) + { + if (!isset($config['class'])) { + $config['class'] = 'Swift_MailTransport'; + } + if (isset($config['plugins'])) { + $plugins = $config['plugins']; + unset($config['plugins']); + } + /** @var \Swift_MailTransport $transport */ + $transport = $this->createSwiftObject($config); + if (isset($plugins)) { + foreach ($plugins as $plugin) { + if (is_array($plugin) && isset($plugin['class'])) { + $plugin = $this->createSwiftObject($plugin); + } + $transport->registerPlugin($plugin); + } + } + + return $transport; + } + + /** + * Creates Swift library object, from given array configuration. + * @param array $config object configuration + * @return Object created object + * @throws \yii\base\InvalidConfigException on invalid configuration. + */ + protected function createSwiftObject(array $config) + { + if (isset($config['class'])) { + $className = $config['class']; + unset($config['class']); + } else { + throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); + } + if (isset($config['constructArgs'])) { + $args = []; + foreach ($config['constructArgs'] as $arg) { + if (is_array($arg) && isset($arg['class'])) { + $args[] = $this->createSwiftObject($arg); + } else { + $args[] = $arg; + } + } + unset($config['constructArgs']); + array_unshift($args, $className); + $object = call_user_func_array(['Yii', 'createObject'], $args); + } else { + $object = new $className; + } + if (!empty($config)) { + foreach ($config as $name => $value) { + if (property_exists($object, $name)) { + $object->$name = $value; + } else { + $setter = 'set' . $name; + if (method_exists($object, $setter) || method_exists($object, '__call')) { + $object->$setter($value); + } else { + throw new InvalidConfigException('Setting unknown property: ' . $className . '::' . $name); + } + } + } + } + + return $object; + } } diff --git a/extensions/swiftmailer/Message.php b/extensions/swiftmailer/Message.php index 7269671bd5c..361a0212f05 100644 --- a/extensions/swiftmailer/Message.php +++ b/extensions/swiftmailer/Message.php @@ -24,281 +24,295 @@ */ class Message extends BaseMessage { - /** - * @var \Swift_Message Swift message instance. - */ - private $_swiftMessage; - - /** - * @return \Swift_Message Swift message instance. - */ - public function getSwiftMessage() - { - if (!is_object($this->_swiftMessage)) { - $this->_swiftMessage = $this->createSwiftMessage(); - } - return $this->_swiftMessage; - } - - /** - * @inheritdoc - */ - public function getCharset() - { - return $this->getSwiftMessage()->getCharset(); - } - - /** - * @inheritdoc - */ - public function setCharset($charset) - { - $this->getSwiftMessage()->setCharset($charset); - return $this; - } - - /** - * @inheritdoc - */ - public function getFrom() - { - return $this->getSwiftMessage()->getFrom(); - } - - /** - * @inheritdoc - */ - public function setFrom($from) - { - $this->getSwiftMessage()->setFrom($from); - return $this; - } - - /** - * @inheritdoc - */ - public function getReplyTo() - { - return $this->getSwiftMessage()->getReplyTo(); - } - - /** - * @inheritdoc - */ - public function setReplyTo($replyTo) - { - $this->getSwiftMessage()->setReplyTo($replyTo); - return $this; - } - - /** - * @inheritdoc - */ - public function getTo() - { - return $this->getSwiftMessage()->getTo(); - } - - /** - * @inheritdoc - */ - public function setTo($to) - { - $this->getSwiftMessage()->setTo($to); - return $this; - } - - /** - * @inheritdoc - */ - public function getCc() - { - return $this->getSwiftMessage()->getCc(); - } - - /** - * @inheritdoc - */ - public function setCc($cc) - { - $this->getSwiftMessage()->setCc($cc); - return $this; - } - - /** - * @inheritdoc - */ - public function getBcc() - { - return $this->getSwiftMessage()->getBcc(); - } - - /** - * @inheritdoc - */ - public function setBcc($bcc) - { - $this->getSwiftMessage()->setBcc($bcc); - return $this; - } - - /** - * @inheritdoc - */ - public function getSubject() - { - return $this->getSwiftMessage()->getSubject(); - } - - /** - * @inheritdoc - */ - public function setSubject($subject) - { - $this->getSwiftMessage()->setSubject($subject); - return $this; - } - - /** - * @inheritdoc - */ - public function setTextBody($text) - { - $this->setBody($text, 'text/plain'); - return $this; - } - - /** - * @inheritdoc - */ - public function setHtmlBody($html) - { - $this->setBody($html, 'text/html'); - return $this; - } - - /** - * Sets the message body. - * If body is already set and its content type matches given one, it will - * be overridden, if content type miss match the multipart message will be composed. - * @param string $body body content. - * @param string $contentType body content type. - */ - protected function setBody($body, $contentType) - { - $message = $this->getSwiftMessage(); - $oldBody = $message->getBody(); - $charset = $message->getCharset(); - if (empty($oldBody)) { - $parts = $message->getChildren(); - $partFound = false; - foreach ($parts as $key => $part) { - if (!($part instanceof \Swift_Mime_Attachment)) { - /* @var \Swift_Mime_MimePart $part */ - if ($part->getContentType() == $contentType) { - $charset = $part->getCharset(); - unset($parts[$key]); - $partFound = true; - break; - } - } - } - if ($partFound) { - reset($parts); - $message->setChildren($parts); - $message->addPart($body, $contentType, $charset); - } else { - $message->setBody($body, $contentType); - } - } else { - $oldContentType = $message->getContentType(); - if ($oldContentType == $contentType) { - $message->setBody($body, $contentType); - } else { - $message->setBody(null); - $message->setContentType(null); - $message->addPart($oldBody, $oldContentType, $charset); - $message->addPart($body, $contentType, $charset); - } - } - } - - /** - * @inheritdoc - */ - public function attach($fileName, array $options = []) - { - $attachment = \Swift_Attachment::fromPath($fileName); - if (!empty($options['fileName'])) { - $attachment->setFilename($options['fileName']); - } - if (!empty($options['contentType'])) { - $attachment->setContentType($options['contentType']); - } - $this->getSwiftMessage()->attach($attachment); - return $this; - } - - /** - * @inheritdoc - */ - public function attachContent($content, array $options = []) - { - $attachment = \Swift_Attachment::newInstance($content); - if (!empty($options['fileName'])) { - $attachment->setFilename($options['fileName']); - } - if (!empty($options['contentType'])) { - $attachment->setContentType($options['contentType']); - } - $this->getSwiftMessage()->attach($attachment); - return $this; - } - - /** - * @inheritdoc - */ - public function embed($fileName, array $options = []) - { - $embedFile = \Swift_EmbeddedFile::fromPath($fileName); - if (!empty($options['fileName'])) { - $embedFile->setFilename($options['fileName']); - } - if (!empty($options['contentType'])) { - $embedFile->setContentType($options['contentType']); - } - return $this->getSwiftMessage()->embed($embedFile); - } - - /** - * @inheritdoc - */ - public function embedContent($content, array $options = []) - { - $embedFile = \Swift_EmbeddedFile::newInstance($content); - if (!empty($options['fileName'])) { - $embedFile->setFilename($options['fileName']); - } - if (!empty($options['contentType'])) { - $embedFile->setContentType($options['contentType']); - } - return $this->getSwiftMessage()->embed($embedFile); - } - - /** - * @inheritdoc - */ - public function toString() - { - return $this->getSwiftMessage()->toString(); - } - - /** - * Creates the Swift email message instance. - * @return \Swift_Message email message instance. - */ - protected function createSwiftMessage() - { - return new \Swift_Message(); - } + /** + * @var \Swift_Message Swift message instance. + */ + private $_swiftMessage; + + /** + * @return \Swift_Message Swift message instance. + */ + public function getSwiftMessage() + { + if (!is_object($this->_swiftMessage)) { + $this->_swiftMessage = $this->createSwiftMessage(); + } + + return $this->_swiftMessage; + } + + /** + * @inheritdoc + */ + public function getCharset() + { + return $this->getSwiftMessage()->getCharset(); + } + + /** + * @inheritdoc + */ + public function setCharset($charset) + { + $this->getSwiftMessage()->setCharset($charset); + + return $this; + } + + /** + * @inheritdoc + */ + public function getFrom() + { + return $this->getSwiftMessage()->getFrom(); + } + + /** + * @inheritdoc + */ + public function setFrom($from) + { + $this->getSwiftMessage()->setFrom($from); + + return $this; + } + + /** + * @inheritdoc + */ + public function getReplyTo() + { + return $this->getSwiftMessage()->getReplyTo(); + } + + /** + * @inheritdoc + */ + public function setReplyTo($replyTo) + { + $this->getSwiftMessage()->setReplyTo($replyTo); + + return $this; + } + + /** + * @inheritdoc + */ + public function getTo() + { + return $this->getSwiftMessage()->getTo(); + } + + /** + * @inheritdoc + */ + public function setTo($to) + { + $this->getSwiftMessage()->setTo($to); + + return $this; + } + + /** + * @inheritdoc + */ + public function getCc() + { + return $this->getSwiftMessage()->getCc(); + } + + /** + * @inheritdoc + */ + public function setCc($cc) + { + $this->getSwiftMessage()->setCc($cc); + + return $this; + } + + /** + * @inheritdoc + */ + public function getBcc() + { + return $this->getSwiftMessage()->getBcc(); + } + + /** + * @inheritdoc + */ + public function setBcc($bcc) + { + $this->getSwiftMessage()->setBcc($bcc); + + return $this; + } + + /** + * @inheritdoc + */ + public function getSubject() + { + return $this->getSwiftMessage()->getSubject(); + } + + /** + * @inheritdoc + */ + public function setSubject($subject) + { + $this->getSwiftMessage()->setSubject($subject); + + return $this; + } + + /** + * @inheritdoc + */ + public function setTextBody($text) + { + $this->setBody($text, 'text/plain'); + + return $this; + } + + /** + * @inheritdoc + */ + public function setHtmlBody($html) + { + $this->setBody($html, 'text/html'); + + return $this; + } + + /** + * Sets the message body. + * If body is already set and its content type matches given one, it will + * be overridden, if content type miss match the multipart message will be composed. + * @param string $body body content. + * @param string $contentType body content type. + */ + protected function setBody($body, $contentType) + { + $message = $this->getSwiftMessage(); + $oldBody = $message->getBody(); + $charset = $message->getCharset(); + if (empty($oldBody)) { + $parts = $message->getChildren(); + $partFound = false; + foreach ($parts as $key => $part) { + if (!($part instanceof \Swift_Mime_Attachment)) { + /* @var \Swift_Mime_MimePart $part */ + if ($part->getContentType() == $contentType) { + $charset = $part->getCharset(); + unset($parts[$key]); + $partFound = true; + break; + } + } + } + if ($partFound) { + reset($parts); + $message->setChildren($parts); + $message->addPart($body, $contentType, $charset); + } else { + $message->setBody($body, $contentType); + } + } else { + $oldContentType = $message->getContentType(); + if ($oldContentType == $contentType) { + $message->setBody($body, $contentType); + } else { + $message->setBody(null); + $message->setContentType(null); + $message->addPart($oldBody, $oldContentType, $charset); + $message->addPart($body, $contentType, $charset); + } + } + } + + /** + * @inheritdoc + */ + public function attach($fileName, array $options = []) + { + $attachment = \Swift_Attachment::fromPath($fileName); + if (!empty($options['fileName'])) { + $attachment->setFilename($options['fileName']); + } + if (!empty($options['contentType'])) { + $attachment->setContentType($options['contentType']); + } + $this->getSwiftMessage()->attach($attachment); + + return $this; + } + + /** + * @inheritdoc + */ + public function attachContent($content, array $options = []) + { + $attachment = \Swift_Attachment::newInstance($content); + if (!empty($options['fileName'])) { + $attachment->setFilename($options['fileName']); + } + if (!empty($options['contentType'])) { + $attachment->setContentType($options['contentType']); + } + $this->getSwiftMessage()->attach($attachment); + + return $this; + } + + /** + * @inheritdoc + */ + public function embed($fileName, array $options = []) + { + $embedFile = \Swift_EmbeddedFile::fromPath($fileName); + if (!empty($options['fileName'])) { + $embedFile->setFilename($options['fileName']); + } + if (!empty($options['contentType'])) { + $embedFile->setContentType($options['contentType']); + } + + return $this->getSwiftMessage()->embed($embedFile); + } + + /** + * @inheritdoc + */ + public function embedContent($content, array $options = []) + { + $embedFile = \Swift_EmbeddedFile::newInstance($content); + if (!empty($options['fileName'])) { + $embedFile->setFilename($options['fileName']); + } + if (!empty($options['contentType'])) { + $embedFile->setContentType($options['contentType']); + } + + return $this->getSwiftMessage()->embed($embedFile); + } + + /** + * @inheritdoc + */ + public function toString() + { + return $this->getSwiftMessage()->toString(); + } + + /** + * Creates the Swift email message instance. + * @return \Swift_Message email message instance. + */ + protected function createSwiftMessage() + { + return new \Swift_Message(); + } } diff --git a/extensions/twig/TwigSimpleFileLoader.php b/extensions/twig/TwigSimpleFileLoader.php index 0780ff7ab60..819c9495bdb 100644 --- a/extensions/twig/TwigSimpleFileLoader.php +++ b/extensions/twig/TwigSimpleFileLoader.php @@ -9,7 +9,6 @@ namespace yii\twig; - /** * Twig view file loader class. * @@ -17,59 +16,59 @@ */ class TwigSimpleFileLoader implements \Twig_LoaderInterface { - /** - * @var string Path to directory - */ - private $_dir; + /** + * @var string Path to directory + */ + private $_dir; - /** - * @param string $dir path to directory - */ - public function __construct($dir) - { - $this->_dir = $dir; - } + /** + * @param string $dir path to directory + */ + public function __construct($dir) + { + $this->_dir = $dir; + } - /** - * Compare a file's freshness with previously stored timestamp - * - * @param string $name file name to check - * @param integer $time timestamp to compare with - * @return boolean true if file is still fresh and not changes, false otherwise - */ - public function isFresh($name, $time) - { - return filemtime($this->getFilePath($name)) <= $time; - } + /** + * Compare a file's freshness with previously stored timestamp + * + * @param string $name file name to check + * @param integer $time timestamp to compare with + * @return boolean true if file is still fresh and not changes, false otherwise + */ + public function isFresh($name, $time) + { + return filemtime($this->getFilePath($name)) <= $time; + } - /** - * Get the source of given file name - * - * @param string $name file name - * @return string contents of given file name - */ - public function getSource($name) - { - return file_get_contents($this->getFilePath($name)); - } + /** + * Get the source of given file name + * + * @param string $name file name + * @return string contents of given file name + */ + public function getSource($name) + { + return file_get_contents($this->getFilePath($name)); + } - /** - * Get unique key that can represent this file uniquely among other files. - * @param string $name - * @return string - */ - public function getCacheKey($name) - { - return $this->getFilePath($name); - } + /** + * Get unique key that can represent this file uniquely among other files. + * @param string $name + * @return string + */ + public function getCacheKey($name) + { + return $this->getFilePath($name); + } - /** - * internally used to get absolute path of given file name - * @param string $name file name - * @return string absolute path of file - */ - protected function getFilePath($name) - { - return $this->_dir . '/' . $name; - } + /** + * internally used to get absolute path of given file name + * @param string $name file name + * @return string absolute path of file + */ + protected function getFilePath($name) + { + return $this->_dir . '/' . $name; + } } diff --git a/extensions/twig/ViewRenderer.php b/extensions/twig/ViewRenderer.php index 2a487654ac5..786d3412827 100644 --- a/extensions/twig/ViewRenderer.php +++ b/extensions/twig/ViewRenderer.php @@ -24,211 +24,211 @@ */ class ViewRenderer extends BaseViewRenderer { - /** - * @var string the directory or path alias pointing to where Twig cache will be stored. - */ - public $cachePath = '@runtime/Twig/cache'; - /** - * @var array Twig options. - * @see http://twig.sensiolabs.org/doc/api.html#environment-options - */ - public $options = []; - /** - * @var array Objects or static classes. - * Keys of the array are names to call in template, values are objects or names of static classes. - * Example: `['html' => '\yii\helpers\Html']`. - * In the template you can use it like this: `{{ html.a('Login', 'site/login') | raw }}`. - */ - public $globals = []; - /** - * @var array Custom functions. - * Keys of the array are names to call in template, values are names of functions or static methods of some class. - * Example: `['rot13' => 'str_rot13', 'a' => '\yii\helpers\Html::a']`. - * In the template you can use it like this: `{{ rot13('test') }}` or `{{ a('Login', 'site/login') | raw }}`. - */ - public $functions = []; - /** - * @var array Custom filters. - * Keys of the array are names to call in template, values are names of functions or static methods of some class. - * Example: `['rot13' => 'str_rot13', 'jsonEncode' => '\yii\helpers\Json::encode']`. - * In the template you can use it like this: `{{ 'test'|rot13 }}` or `{{ model|jsonEncode }}`. - */ - public $filters = []; - /** - * @var array Custom extensions. - * Example: `['Twig_Extension_Sandbox', 'Twig_Extension_Text']` - */ - public $extensions = []; - /** - * @var array Twig lexer options. - * Example: Smarty-like syntax: - * ```php - * [ - * 'tag_comment' => ['{*', '*}'], - * 'tag_block' => ['{', '}'], - * 'tag_variable' => ['{$', '}'] - * ] - * ``` - * @see http://twig.sensiolabs.org/doc/recipes.html#customizing-the-syntax - */ - public $lexerOptions = []; - /** - * @var \Twig_Environment twig environment object that do all rendering twig templates - */ - public $twig; - - - public function init() - { - $this->twig = new \Twig_Environment(null, array_merge([ - 'cache' => Yii::getAlias($this->cachePath), - 'charset' => Yii::$app->charset, - ], $this->options)); - - // Adding custom extensions - if (!empty($this->extensions)) { - foreach ($this->extensions as $extension) { - $this->twig->addExtension(new $extension()); - } - } - - // Adding custom globals (objects or static classes) - if (!empty($this->globals)) { - $this->addGlobals($this->globals); - } - - // Adding custom functions - if (!empty($this->functions)) { - $this->addFunctions($this->functions); - } - - // Adding custom filters - if (!empty($this->filters)) { - $this->addFilters($this->filters); - } - - // Adding custom extensions - if (!empty($this->extensions)) { - $this->addExtensions($this->extensions); - } - - // Change lexer syntax - if (!empty($this->lexerOptions)) { - $this->setLexerOptions($this->lexerOptions); - } - - // Adding global 'void' function (usage: {{void(App.clientScript.registerScriptFile(...))}}) - $this->twig->addFunction('void', new \Twig_Function_Function(function ($argument) { - })); - - $this->twig->addFunction('path', new \Twig_Function_Function(function ($path, $args = []) { - return Url::to(array_merge([$path], $args)); - })); - - $this->twig->addGlobal('app', \Yii::$app); - } - - /** - * Renders a view file. - * - * This method is invoked by [[View]] whenever it tries to render a view. - * Child classes must implement this method to render the given view file. - * - * @param View $view the view object used for rendering the file. - * @param string $file the view file. - * @param array $params the parameters to be passed to the view file. - * - * @return string the rendering result - */ - public function render($view, $file, $params) - { - $this->twig->addGlobal('this', $view); - $this->twig->setLoader(new TwigSimpleFileLoader(dirname($file))); - return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params); - } - - /** - * Adds global objects or static classes - * @param array $globals @see self::$globals - */ - public function addGlobals($globals) - { - foreach ($globals as $name => $value) { - if (!is_object($value)) { - $value = new ViewRendererStaticClassProxy($value); - } - $this->twig->addGlobal($name, $value); - } - } - - /** - * Adds custom functions - * @param array $functions @see self::$functions - */ - public function addFunctions($functions) - { - $this->_addCustom('Function', $functions); - } - - /** - * Adds custom filters - * @param array $filters @see self::$filters - */ - public function addFilters($filters) - { - $this->_addCustom('Filter', $filters); - } - - /** - * Adds custom extensions - * @param array $extensions @see self::$extensions - */ - public function addExtensions($extensions) - { - foreach ($extensions as $extName) { - $this->twig->addExtension(new $extName()); - } - } - - /** - * Sets Twig lexer options to change templates syntax - * @param array $options @see self::$lexerOptions - */ - public function setLexerOptions($options) - { - $lexer = new \Twig_Lexer($this->twig, $options); - $this->twig->setLexer($lexer); - } - - /** - * Adds custom function or filter - * @param string $classType 'Function' or 'Filter' - * @param array $elements Parameters of elements to add - * @throws \Exception - */ - private function _addCustom($classType, $elements) - { - $classFunction = 'Twig_' . $classType . '_Function'; - - foreach ($elements as $name => $func) { - $twigElement = null; - - switch ($func) { - // Just a name of function - case is_string($func): - $twigElement = new $classFunction($func); - break; - // Name of function + options array - case is_array($func) && is_string($func[0]) && isset($func[1]) && is_array($func[1]): - $twigElement = new $classFunction($func[0], $func[1]); - break; - } - - if ($twigElement !== null) { - $this->twig->{'add'.$classType}($name, $twigElement); - } else { - throw new \Exception("Incorrect options for \"$classType\" $name."); - } - } - } + /** + * @var string the directory or path alias pointing to where Twig cache will be stored. + */ + public $cachePath = '@runtime/Twig/cache'; + /** + * @var array Twig options. + * @see http://twig.sensiolabs.org/doc/api.html#environment-options + */ + public $options = []; + /** + * @var array Objects or static classes. + * Keys of the array are names to call in template, values are objects or names of static classes. + * Example: `['html' => '\yii\helpers\Html']`. + * In the template you can use it like this: `{{ html.a('Login', 'site/login') | raw }}`. + */ + public $globals = []; + /** + * @var array Custom functions. + * Keys of the array are names to call in template, values are names of functions or static methods of some class. + * Example: `['rot13' => 'str_rot13', 'a' => '\yii\helpers\Html::a']`. + * In the template you can use it like this: `{{ rot13('test') }}` or `{{ a('Login', 'site/login') | raw }}`. + */ + public $functions = []; + /** + * @var array Custom filters. + * Keys of the array are names to call in template, values are names of functions or static methods of some class. + * Example: `['rot13' => 'str_rot13', 'jsonEncode' => '\yii\helpers\Json::encode']`. + * In the template you can use it like this: `{{ 'test'|rot13 }}` or `{{ model|jsonEncode }}`. + */ + public $filters = []; + /** + * @var array Custom extensions. + * Example: `['Twig_Extension_Sandbox', 'Twig_Extension_Text']` + */ + public $extensions = []; + /** + * @var array Twig lexer options. + * Example: Smarty-like syntax: + * ```php + * [ + * 'tag_comment' => ['{*', '*}'], + * 'tag_block' => ['{', '}'], + * 'tag_variable' => ['{$', '}'] + * ] + * ``` + * @see http://twig.sensiolabs.org/doc/recipes.html#customizing-the-syntax + */ + public $lexerOptions = []; + /** + * @var \Twig_Environment twig environment object that do all rendering twig templates + */ + public $twig; + + public function init() + { + $this->twig = new \Twig_Environment(null, array_merge([ + 'cache' => Yii::getAlias($this->cachePath), + 'charset' => Yii::$app->charset, + ], $this->options)); + + // Adding custom extensions + if (!empty($this->extensions)) { + foreach ($this->extensions as $extension) { + $this->twig->addExtension(new $extension()); + } + } + + // Adding custom globals (objects or static classes) + if (!empty($this->globals)) { + $this->addGlobals($this->globals); + } + + // Adding custom functions + if (!empty($this->functions)) { + $this->addFunctions($this->functions); + } + + // Adding custom filters + if (!empty($this->filters)) { + $this->addFilters($this->filters); + } + + // Adding custom extensions + if (!empty($this->extensions)) { + $this->addExtensions($this->extensions); + } + + // Change lexer syntax + if (!empty($this->lexerOptions)) { + $this->setLexerOptions($this->lexerOptions); + } + + // Adding global 'void' function (usage: {{void(App.clientScript.registerScriptFile(...))}}) + $this->twig->addFunction('void', new \Twig_Function_Function(function ($argument) { + })); + + $this->twig->addFunction('path', new \Twig_Function_Function(function ($path, $args = []) { + return Url::to(array_merge([$path], $args)); + })); + + $this->twig->addGlobal('app', \Yii::$app); + } + + /** + * Renders a view file. + * + * This method is invoked by [[View]] whenever it tries to render a view. + * Child classes must implement this method to render the given view file. + * + * @param View $view the view object used for rendering the file. + * @param string $file the view file. + * @param array $params the parameters to be passed to the view file. + * + * @return string the rendering result + */ + public function render($view, $file, $params) + { + $this->twig->addGlobal('this', $view); + $this->twig->setLoader(new TwigSimpleFileLoader(dirname($file))); + + return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params); + } + + /** + * Adds global objects or static classes + * @param array $globals @see self::$globals + */ + public function addGlobals($globals) + { + foreach ($globals as $name => $value) { + if (!is_object($value)) { + $value = new ViewRendererStaticClassProxy($value); + } + $this->twig->addGlobal($name, $value); + } + } + + /** + * Adds custom functions + * @param array $functions @see self::$functions + */ + public function addFunctions($functions) + { + $this->_addCustom('Function', $functions); + } + + /** + * Adds custom filters + * @param array $filters @see self::$filters + */ + public function addFilters($filters) + { + $this->_addCustom('Filter', $filters); + } + + /** + * Adds custom extensions + * @param array $extensions @see self::$extensions + */ + public function addExtensions($extensions) + { + foreach ($extensions as $extName) { + $this->twig->addExtension(new $extName()); + } + } + + /** + * Sets Twig lexer options to change templates syntax + * @param array $options @see self::$lexerOptions + */ + public function setLexerOptions($options) + { + $lexer = new \Twig_Lexer($this->twig, $options); + $this->twig->setLexer($lexer); + } + + /** + * Adds custom function or filter + * @param string $classType 'Function' or 'Filter' + * @param array $elements Parameters of elements to add + * @throws \Exception + */ + private function _addCustom($classType, $elements) + { + $classFunction = 'Twig_' . $classType . '_Function'; + + foreach ($elements as $name => $func) { + $twigElement = null; + + switch ($func) { + // Just a name of function + case is_string($func): + $twigElement = new $classFunction($func); + break; + // Name of function + options array + case is_array($func) && is_string($func[0]) && isset($func[1]) && is_array($func[1]): + $twigElement = new $classFunction($func[0], $func[1]); + break; + } + + if ($twigElement !== null) { + $this->twig->{'add'.$classType}($name, $twigElement); + } else { + throw new \Exception("Incorrect options for \"$classType\" $name."); + } + } + } } diff --git a/extensions/twig/ViewRendererStaticClassProxy.php b/extensions/twig/ViewRendererStaticClassProxy.php index 343cdadcbed..e25b430f073 100644 --- a/extensions/twig/ViewRendererStaticClassProxy.php +++ b/extensions/twig/ViewRendererStaticClassProxy.php @@ -17,28 +17,30 @@ */ class ViewRendererStaticClassProxy { - private $_staticClassName; - - public function __construct($staticClassName) - { - $this->_staticClassName = $staticClassName; - } - - public function __get($property) - { - $class = new \ReflectionClass($this->_staticClassName); - return $class->getStaticPropertyValue($property); - } - - public function __set($property, $value) - { - $class = new \ReflectionClass($this->_staticClassName); - $class->setStaticPropertyValue($property, $value); - return $value; - } - - public function __call($method, $arguments) - { - return call_user_func_array([$this->_staticClassName, $method], $arguments); - } + private $_staticClassName; + + public function __construct($staticClassName) + { + $this->_staticClassName = $staticClassName; + } + + public function __get($property) + { + $class = new \ReflectionClass($this->_staticClassName); + + return $class->getStaticPropertyValue($property); + } + + public function __set($property, $value) + { + $class = new \ReflectionClass($this->_staticClassName); + $class->setStaticPropertyValue($property, $value); + + return $value; + } + + public function __call($method, $arguments) + { + return call_user_func_array([$this->_staticClassName, $method], $arguments); + } } diff --git a/framework/BaseYii.php b/framework/BaseYii.php index c70808e0bd7..3457b523640 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -46,7 +46,6 @@ */ defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', true); - /** * BaseYii is the core helper class for the Yii framework. * @@ -58,477 +57,481 @@ */ class BaseYii { - /** - * @var array class map used by the Yii autoloading mechanism. - * The array keys are the class names (without leading backslashes), and the array values - * are the corresponding class file paths (or path aliases). This property mainly affects - * how [[autoload()]] works. - * @see autoload() - */ - public static $classMap = []; - /** - * @var \yii\console\Application|\yii\web\Application the application instance - */ - public static $app; - /** - * @var array registered path aliases - * @see getAlias() - * @see setAlias() - */ - public static $aliases = ['@yii' => __DIR__]; - /** - * @var array initial property values that will be applied to objects newly created via [[createObject]]. - * The array keys are class names without leading backslashes "\", and the array values are the corresponding - * name-value pairs for initializing the created class instances. For example, - * - * ~~~ - * [ - * 'Bar' => [ - * 'prop1' => 'value1', - * 'prop2' => 'value2', - * ], - * 'mycompany\foo\Car' => [ - * 'prop1' => 'value1', - * 'prop2' => 'value2', - * ], - * ] - * ~~~ - * - * @see createObject() - */ - public static $objectConfig = []; - - - /** - * Returns a string representing the current version of the Yii framework. - * @return string the version of Yii framework - */ - public static function getVersion() - { - return '2.0.0-dev'; - } - - /** - * Translates a path alias into an actual path. - * - * The translation is done according to the following procedure: - * - * 1. If the given alias does not start with '@', it is returned back without change; - * 2. Otherwise, look for the longest registered alias that matches the beginning part - * of the given alias. If it exists, replace the matching part of the given alias with - * the corresponding registered path. - * 3. Throw an exception or return false, depending on the `$throwException` parameter. - * - * For example, by default '@yii' is registered as the alias to the Yii framework directory, - * say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'. - * - * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config' - * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path. - * This is because the longest alias takes precedence. - * - * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced - * instead of '@foo/bar', because '/' serves as the boundary character. - * - * Note, this method does not check if the returned path exists or not. - * - * @param string $alias the alias to be translated. - * @param boolean $throwException whether to throw an exception if the given alias is invalid. - * If this is false and an invalid alias is given, false will be returned by this method. - * @return string|boolean the path corresponding to the alias, false if the root alias is not previously registered. - * @throws InvalidParamException if the alias is invalid while $throwException is true. - * @see setAlias() - */ - public static function getAlias($alias, $throwException = true) - { - if (strncmp($alias, '@', 1)) { - // not an alias - return $alias; - } - - $pos = strpos($alias, '/'); - $root = $pos === false ? $alias : substr($alias, 0, $pos); - - if (isset(static::$aliases[$root])) { - if (is_string(static::$aliases[$root])) { - return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos); - } else { - foreach (static::$aliases[$root] as $name => $path) { - if (strpos($alias . '/', $name . '/') === 0) { - return $path . substr($alias, strlen($name)); - } - } - } - } - - if ($throwException) { - throw new InvalidParamException("Invalid path alias: $alias"); - } else { - return false; - } - } - - /** - * Returns the root alias part of a given alias. - * A root alias is an alias that has been registered via [[setAlias()]] previously. - * If a given alias matches multiple root aliases, the longest one will be returned. - * @param string $alias the alias - * @return string|boolean the root alias, or false if no root alias is found - */ - public static function getRootAlias($alias) - { - $pos = strpos($alias, '/'); - $root = $pos === false ? $alias : substr($alias, 0, $pos); - - if (isset(static::$aliases[$root])) { - if (is_string(static::$aliases[$root])) { - return $root; - } else { - foreach (static::$aliases[$root] as $name => $path) { - if (strpos($alias . '/', $name . '/') === 0) { - return $name; - } - } - } - } - return false; - } - - /** - * Registers a path alias. - * - * A path alias is a short name representing a long path (a file path, a URL, etc.) - * For example, we use '@yii' as the alias of the path to the Yii framework directory. - * - * A path alias must start with the character '@' so that it can be easily differentiated - * from non-alias paths. - * - * Note that this method does not check if the given path exists or not. All it does is - * to associate the alias with the path. - * - * Any trailing '/' and '\' characters in the given path will be trimmed. - * - * @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character. - * It may contain the forward slash '/' which serves as boundary character when performing - * alias translation by [[getAlias()]]. - * @param string $path the path corresponding to the alias. Trailing '/' and '\' characters - * will be trimmed. This can be - * - * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) - * - a URL (e.g. `http://www.yiiframework.com`) - * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the - * actual path first by calling [[getAlias()]]. - * - * @throws InvalidParamException if $path is an invalid alias. - * @see getAlias() - */ - public static function setAlias($alias, $path) - { - if (strncmp($alias, '@', 1)) { - $alias = '@' . $alias; - } - $pos = strpos($alias, '/'); - $root = $pos === false ? $alias : substr($alias, 0, $pos); - if ($path !== null) { - $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); - if (!isset(static::$aliases[$root])) { - if ($pos === false) { - static::$aliases[$root] = $path; - } else { - static::$aliases[$root] = [$alias => $path]; - } - } elseif (is_string(static::$aliases[$root])) { - if ($pos === false) { - static::$aliases[$root] = $path; - } else { - static::$aliases[$root] = [ - $alias => $path, - $root => static::$aliases[$root], - ]; - } - } else { - static::$aliases[$root][$alias] = $path; - krsort(static::$aliases[$root]); - } - } elseif (isset(static::$aliases[$root])) { - if (is_array(static::$aliases[$root])) { - unset(static::$aliases[$root][$alias]); - } elseif ($pos === false) { - unset(static::$aliases[$root]); - } - } - } - - /** - * Class autoload loader. - * This method is invoked automatically when PHP sees an unknown class. - * The method will attempt to include the class file according to the following procedure: - * - * 1. Search in [[classMap]]; - * 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt - * to include the file associated with the corresponding path alias - * (e.g. `@yii/base/Component.php`); - * - * This autoloader allows loading classes that follow the [PSR-4 standard](http://www.php-fig.org/psr/psr-4/) - * and have its top-level namespace or sub-namespaces defined as path aliases. - * - * Example: When aliases `@yii` and `@yii/bootstrap` are defined, classes in the `yii\bootstrap` namespace - * will be loaded using the `@yii/bootstrap` alias which points to the directory where bootstrap extension - * files are installed and all classes from other `yii` namespaces will be loaded from the yii framework directory. - * - * @param string $className the fully qualified class name without a leading backslash "\" - * @throws UnknownClassException if the class does not exist in the class file - */ - public static function autoload($className) - { - if (isset(static::$classMap[$className])) { - $classFile = static::$classMap[$className]; - if ($classFile[0] === '@') { - $classFile = static::getAlias($classFile); - } - } elseif (strpos($className, '\\') !== false) { - $classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false); - if ($classFile === false || !is_file($classFile)) { - return; - } - } else { - return; - } - - include($classFile); - - if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && !trait_exists($className, false)) { - throw new UnknownClassException("Unable to find '$className' in file: $classFile. Namespace missing?"); - } - } - - /** - * Creates a new object using the given configuration. - * - * The configuration can be either a string or an array. - * If a string, it is treated as the *object class*; if an array, - * it must contain a `class` element specifying the *object class*, and - * the rest of the name-value pairs in the array will be used to initialize - * the corresponding object properties. - * - * Below are some usage examples: - * - * ~~~ - * $object = \Yii::createObject('app\components\GoogleMap'); - * $object = \Yii::createObject([ - * 'class' => 'app\components\GoogleMap', - * 'apiKey' => 'xyz', - * ]); - * ~~~ - * - * This method can be used to create any object as long as the object's constructor is - * defined like the following: - * - * ~~~ - * public function __construct(..., $config = []) { - * } - * ~~~ - * - * The method will pass the given configuration as the last parameter of the constructor, - * and any additional parameters to this method will be passed as the rest of the constructor parameters. - * - * @param string|array $config the configuration. It can be either a string representing the class name - * or an array representing the object configuration. - * @return mixed the created object - * @throws InvalidConfigException if the configuration is invalid. - */ - public static function createObject($config) - { - static $reflections = []; - - if (is_string($config)) { - $class = $config; - $config = []; - } elseif (isset($config['class'])) { - $class = $config['class']; - unset($config['class']); - } else { - throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); - } - - $class = ltrim($class, '\\'); - - if (isset(static::$objectConfig[$class])) { - $config = array_merge(static::$objectConfig[$class], $config); - } - - if (func_num_args() > 1) { - /** @var \ReflectionClass $reflection */ - if (isset($reflections[$class])) { - $reflection = $reflections[$class]; - } else { - $reflection = $reflections[$class] = new \ReflectionClass($class); - } - $args = func_get_args(); - array_shift($args); // remove $config - if (!empty($config)) { - $args[] = $config; - } - return $reflection->newInstanceArgs($args); - } else { - return empty($config) ? new $class : new $class($config); - } - } - - /** - * Logs a trace message. - * Trace messages are logged mainly for development purpose to see - * the execution work flow of some code. - * @param string $message the message to be logged. - * @param string $category the category of the message. - */ - public static function trace($message, $category = 'application') - { - if (YII_DEBUG) { - static::$app->getLog()->log($message, Logger::LEVEL_TRACE, $category); - } - } - - /** - * Logs an error message. - * An error message is typically logged when an unrecoverable error occurs - * during the execution of an application. - * @param string $message the message to be logged. - * @param string $category the category of the message. - */ - public static function error($message, $category = 'application') - { - static::$app->getLog()->log($message, Logger::LEVEL_ERROR, $category); - } - - /** - * Logs a warning message. - * A warning message is typically logged when an error occurs while the execution - * can still continue. - * @param string $message the message to be logged. - * @param string $category the category of the message. - */ - public static function warning($message, $category = 'application') - { - static::$app->getLog()->log($message, Logger::LEVEL_WARNING, $category); - } - - /** - * Logs an informative message. - * An informative message is typically logged by an application to keep record of - * something important (e.g. an administrator logs in). - * @param string $message the message to be logged. - * @param string $category the category of the message. - */ - public static function info($message, $category = 'application') - { - static::$app->getLog()->log($message, Logger::LEVEL_INFO, $category); - } - - /** - * Marks the beginning of a code block for profiling. - * This has to be matched with a call to [[endProfile]] with the same category name. - * The begin- and end- calls must also be properly nested. For example, - * - * ~~~ - * \Yii::beginProfile('block1'); - * // some code to be profiled - * \Yii::beginProfile('block2'); - * // some other code to be profiled - * \Yii::endProfile('block2'); - * \Yii::endProfile('block1'); - * ~~~ - * @param string $token token for the code block - * @param string $category the category of this log message - * @see endProfile() - */ - public static function beginProfile($token, $category = 'application') - { - static::$app->getLog()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category); - } - - /** - * Marks the end of a code block for profiling. - * This has to be matched with a previous call to [[beginProfile]] with the same category name. - * @param string $token token for the code block - * @param string $category the category of this log message - * @see beginProfile() - */ - public static function endProfile($token, $category = 'application') - { - static::$app->getLog()->log($token, Logger::LEVEL_PROFILE_END, $category); - } - - /** - * Returns an HTML hyperlink that can be displayed on your Web page showing "Powered by Yii Framework" information. - * @return string an HTML hyperlink that can be displayed on your Web page showing "Powered by Yii Framework" information - */ - public static function powered() - { - return 'Powered by Yii Framework'; - } - - /** - * Translates a message to the specified language. - * - * This is a shortcut method of [[\yii\i18n\I18N::translate()]]. - * - * The translation will be conducted according to the message category and the target language will be used. - * - * You can add parameters to a translation message that will be substituted with the corresponding value after - * translation. The format for this is to use curly brackets around the parameter name as you can see in the following example: - * - * ```php - * $username = 'Alexander'; - * echo \Yii::t('app', 'Hello, {username}!', ['username' => $username]); - * ``` - * - * Further formatting of message parameters is supported using the [PHP intl extensions](http://www.php.net/manual/en/intro.intl.php) - * message formatter. See [[\yii\i18n\I18N::translate()]] for more details. - * - * @param string $category the message category. - * @param string $message the message to be translated. - * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. - * @param string $language the language code (e.g. `en-US`, `en`). If this is null, the current - * [[\yii\base\Application::language|application language]] will be used. - * @return string the translated message. - */ - public static function t($category, $message, $params = [], $language = null) - { - if (static::$app !== null) { - return static::$app->getI18n()->translate($category, $message, $params, $language ?: static::$app->language); - } else { - $p = []; - foreach ((array) $params as $name => $value) { - $p['{' . $name . '}'] = $value; - } - return ($p === []) ? $message : strtr($message, $p); - } - } - - /** - * Configures an object with the initial property values. - * @param object $object the object to be configured - * @param array $properties the property initial values given in terms of name-value pairs. - * @return object the object itself - */ - public static function configure($object, $properties) - { - foreach ($properties as $name => $value) { - $object->$name = $value; - } - return $object; - } - - /** - * Returns the public member variables of an object. - * This method is provided such that we can get the public member variables of an object. - * It is different from "get_object_vars()" because the latter will return private - * and protected variables if it is called within the object itself. - * @param object $object the object to be handled - * @return array the public member variables of the object - */ - public static function getObjectVars($object) - { - return get_object_vars($object); - } + /** + * @var array class map used by the Yii autoloading mechanism. + * The array keys are the class names (without leading backslashes), and the array values + * are the corresponding class file paths (or path aliases). This property mainly affects + * how [[autoload()]] works. + * @see autoload() + */ + public static $classMap = []; + /** + * @var \yii\console\Application|\yii\web\Application the application instance + */ + public static $app; + /** + * @var array registered path aliases + * @see getAlias() + * @see setAlias() + */ + public static $aliases = ['@yii' => __DIR__]; + /** + * @var array initial property values that will be applied to objects newly created via [[createObject]]. + * The array keys are class names without leading backslashes "\", and the array values are the corresponding + * name-value pairs for initializing the created class instances. For example, + * + * ~~~ + * [ + * 'Bar' => [ + * 'prop1' => 'value1', + * 'prop2' => 'value2', + * ], + * 'mycompany\foo\Car' => [ + * 'prop1' => 'value1', + * 'prop2' => 'value2', + * ], + * ] + * ~~~ + * + * @see createObject() + */ + public static $objectConfig = []; + + + /** + * Returns a string representing the current version of the Yii framework. + * @return string the version of Yii framework + */ + public static function getVersion() + { + return '2.0.0-dev'; + } + + /** + * Translates a path alias into an actual path. + * + * The translation is done according to the following procedure: + * + * 1. If the given alias does not start with '@', it is returned back without change; + * 2. Otherwise, look for the longest registered alias that matches the beginning part + * of the given alias. If it exists, replace the matching part of the given alias with + * the corresponding registered path. + * 3. Throw an exception or return false, depending on the `$throwException` parameter. + * + * For example, by default '@yii' is registered as the alias to the Yii framework directory, + * say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'. + * + * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config' + * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path. + * This is because the longest alias takes precedence. + * + * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced + * instead of '@foo/bar', because '/' serves as the boundary character. + * + * Note, this method does not check if the returned path exists or not. + * + * @param string $alias the alias to be translated. + * @param boolean $throwException whether to throw an exception if the given alias is invalid. + * If this is false and an invalid alias is given, false will be returned by this method. + * @return string|boolean the path corresponding to the alias, false if the root alias is not previously registered. + * @throws InvalidParamException if the alias is invalid while $throwException is true. + * @see setAlias() + */ + public static function getAlias($alias, $throwException = true) + { + if (strncmp($alias, '@', 1)) { + // not an alias + return $alias; + } + + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(static::$aliases[$root])) { + if (is_string(static::$aliases[$root])) { + return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos); + } else { + foreach (static::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $path . substr($alias, strlen($name)); + } + } + } + } + + if ($throwException) { + throw new InvalidParamException("Invalid path alias: $alias"); + } else { + return false; + } + } + + /** + * Returns the root alias part of a given alias. + * A root alias is an alias that has been registered via [[setAlias()]] previously. + * If a given alias matches multiple root aliases, the longest one will be returned. + * @param string $alias the alias + * @return string|boolean the root alias, or false if no root alias is found + */ + public static function getRootAlias($alias) + { + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(static::$aliases[$root])) { + if (is_string(static::$aliases[$root])) { + return $root; + } else { + foreach (static::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $name; + } + } + } + } + + return false; + } + + /** + * Registers a path alias. + * + * A path alias is a short name representing a long path (a file path, a URL, etc.) + * For example, we use '@yii' as the alias of the path to the Yii framework directory. + * + * A path alias must start with the character '@' so that it can be easily differentiated + * from non-alias paths. + * + * Note that this method does not check if the given path exists or not. All it does is + * to associate the alias with the path. + * + * Any trailing '/' and '\' characters in the given path will be trimmed. + * + * @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character. + * It may contain the forward slash '/' which serves as boundary character when performing + * alias translation by [[getAlias()]]. + * @param string $path the path corresponding to the alias. Trailing '/' and '\' characters + * will be trimmed. This can be + * + * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) + * - a URL (e.g. `http://www.yiiframework.com`) + * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the + * actual path first by calling [[getAlias()]]. + * + * @throws InvalidParamException if $path is an invalid alias. + * @see getAlias() + */ + public static function setAlias($alias, $path) + { + if (strncmp($alias, '@', 1)) { + $alias = '@' . $alias; + } + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + if ($path !== null) { + $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); + if (!isset(static::$aliases[$root])) { + if ($pos === false) { + static::$aliases[$root] = $path; + } else { + static::$aliases[$root] = [$alias => $path]; + } + } elseif (is_string(static::$aliases[$root])) { + if ($pos === false) { + static::$aliases[$root] = $path; + } else { + static::$aliases[$root] = [ + $alias => $path, + $root => static::$aliases[$root], + ]; + } + } else { + static::$aliases[$root][$alias] = $path; + krsort(static::$aliases[$root]); + } + } elseif (isset(static::$aliases[$root])) { + if (is_array(static::$aliases[$root])) { + unset(static::$aliases[$root][$alias]); + } elseif ($pos === false) { + unset(static::$aliases[$root]); + } + } + } + + /** + * Class autoload loader. + * This method is invoked automatically when PHP sees an unknown class. + * The method will attempt to include the class file according to the following procedure: + * + * 1. Search in [[classMap]]; + * 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt + * to include the file associated with the corresponding path alias + * (e.g. `@yii/base/Component.php`); + * + * This autoloader allows loading classes that follow the [PSR-4 standard](http://www.php-fig.org/psr/psr-4/) + * and have its top-level namespace or sub-namespaces defined as path aliases. + * + * Example: When aliases `@yii` and `@yii/bootstrap` are defined, classes in the `yii\bootstrap` namespace + * will be loaded using the `@yii/bootstrap` alias which points to the directory where bootstrap extension + * files are installed and all classes from other `yii` namespaces will be loaded from the yii framework directory. + * + * @param string $className the fully qualified class name without a leading backslash "\" + * @throws UnknownClassException if the class does not exist in the class file + */ + public static function autoload($className) + { + if (isset(static::$classMap[$className])) { + $classFile = static::$classMap[$className]; + if ($classFile[0] === '@') { + $classFile = static::getAlias($classFile); + } + } elseif (strpos($className, '\\') !== false) { + $classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false); + if ($classFile === false || !is_file($classFile)) { + return; + } + } else { + return; + } + + include($classFile); + + if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && !trait_exists($className, false)) { + throw new UnknownClassException("Unable to find '$className' in file: $classFile. Namespace missing?"); + } + } + + /** + * Creates a new object using the given configuration. + * + * The configuration can be either a string or an array. + * If a string, it is treated as the *object class*; if an array, + * it must contain a `class` element specifying the *object class*, and + * the rest of the name-value pairs in the array will be used to initialize + * the corresponding object properties. + * + * Below are some usage examples: + * + * ~~~ + * $object = \Yii::createObject('app\components\GoogleMap'); + * $object = \Yii::createObject([ + * 'class' => 'app\components\GoogleMap', + * 'apiKey' => 'xyz', + * ]); + * ~~~ + * + * This method can be used to create any object as long as the object's constructor is + * defined like the following: + * + * ~~~ + * public function __construct(..., $config = []) { + * } + * ~~~ + * + * The method will pass the given configuration as the last parameter of the constructor, + * and any additional parameters to this method will be passed as the rest of the constructor parameters. + * + * @param string|array $config the configuration. It can be either a string representing the class name + * or an array representing the object configuration. + * @return mixed the created object + * @throws InvalidConfigException if the configuration is invalid. + */ + public static function createObject($config) + { + static $reflections = []; + + if (is_string($config)) { + $class = $config; + $config = []; + } elseif (isset($config['class'])) { + $class = $config['class']; + unset($config['class']); + } else { + throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); + } + + $class = ltrim($class, '\\'); + + if (isset(static::$objectConfig[$class])) { + $config = array_merge(static::$objectConfig[$class], $config); + } + + if (func_num_args() > 1) { + /** @var \ReflectionClass $reflection */ + if (isset($reflections[$class])) { + $reflection = $reflections[$class]; + } else { + $reflection = $reflections[$class] = new \ReflectionClass($class); + } + $args = func_get_args(); + array_shift($args); // remove $config + if (!empty($config)) { + $args[] = $config; + } + + return $reflection->newInstanceArgs($args); + } else { + return empty($config) ? new $class : new $class($config); + } + } + + /** + * Logs a trace message. + * Trace messages are logged mainly for development purpose to see + * the execution work flow of some code. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public static function trace($message, $category = 'application') + { + if (YII_DEBUG) { + static::$app->getLog()->log($message, Logger::LEVEL_TRACE, $category); + } + } + + /** + * Logs an error message. + * An error message is typically logged when an unrecoverable error occurs + * during the execution of an application. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public static function error($message, $category = 'application') + { + static::$app->getLog()->log($message, Logger::LEVEL_ERROR, $category); + } + + /** + * Logs a warning message. + * A warning message is typically logged when an error occurs while the execution + * can still continue. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public static function warning($message, $category = 'application') + { + static::$app->getLog()->log($message, Logger::LEVEL_WARNING, $category); + } + + /** + * Logs an informative message. + * An informative message is typically logged by an application to keep record of + * something important (e.g. an administrator logs in). + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public static function info($message, $category = 'application') + { + static::$app->getLog()->log($message, Logger::LEVEL_INFO, $category); + } + + /** + * Marks the beginning of a code block for profiling. + * This has to be matched with a call to [[endProfile]] with the same category name. + * The begin- and end- calls must also be properly nested. For example, + * + * ~~~ + * \Yii::beginProfile('block1'); + * // some code to be profiled + * \Yii::beginProfile('block2'); + * // some other code to be profiled + * \Yii::endProfile('block2'); + * \Yii::endProfile('block1'); + * ~~~ + * @param string $token token for the code block + * @param string $category the category of this log message + * @see endProfile() + */ + public static function beginProfile($token, $category = 'application') + { + static::$app->getLog()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category); + } + + /** + * Marks the end of a code block for profiling. + * This has to be matched with a previous call to [[beginProfile]] with the same category name. + * @param string $token token for the code block + * @param string $category the category of this log message + * @see beginProfile() + */ + public static function endProfile($token, $category = 'application') + { + static::$app->getLog()->log($token, Logger::LEVEL_PROFILE_END, $category); + } + + /** + * Returns an HTML hyperlink that can be displayed on your Web page showing "Powered by Yii Framework" information. + * @return string an HTML hyperlink that can be displayed on your Web page showing "Powered by Yii Framework" information + */ + public static function powered() + { + return 'Powered by Yii Framework'; + } + + /** + * Translates a message to the specified language. + * + * This is a shortcut method of [[\yii\i18n\I18N::translate()]]. + * + * The translation will be conducted according to the message category and the target language will be used. + * + * You can add parameters to a translation message that will be substituted with the corresponding value after + * translation. The format for this is to use curly brackets around the parameter name as you can see in the following example: + * + * ```php + * $username = 'Alexander'; + * echo \Yii::t('app', 'Hello, {username}!', ['username' => $username]); + * ``` + * + * Further formatting of message parameters is supported using the [PHP intl extensions](http://www.php.net/manual/en/intro.intl.php) + * message formatter. See [[\yii\i18n\I18N::translate()]] for more details. + * + * @param string $category the message category. + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en-US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. + */ + public static function t($category, $message, $params = [], $language = null) + { + if (static::$app !== null) { + return static::$app->getI18n()->translate($category, $message, $params, $language ?: static::$app->language); + } else { + $p = []; + foreach ((array) $params as $name => $value) { + $p['{' . $name . '}'] = $value; + } + + return ($p === []) ? $message : strtr($message, $p); + } + } + + /** + * Configures an object with the initial property values. + * @param object $object the object to be configured + * @param array $properties the property initial values given in terms of name-value pairs. + * @return object the object itself + */ + public static function configure($object, $properties) + { + foreach ($properties as $name => $value) { + $object->$name = $value; + } + + return $object; + } + + /** + * Returns the public member variables of an object. + * This method is provided such that we can get the public member variables of an object. + * It is different from "get_object_vars()" because the latter will return private + * and protected variables if it is called within the object itself. + * @param object $object the object to be handled + * @return array the public member variables of the object + */ + public static function getObjectVars($object) + { + return get_object_vars($object); + } } diff --git a/framework/base/Action.php b/framework/base/Action.php index fd9c3613cd3..4299d0a2941 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -36,79 +36,80 @@ */ class Action extends Component { - /** - * @var string ID of the action - */ - public $id; - /** - * @var Controller the controller that owns this action - */ - public $controller; + /** + * @var string ID of the action + */ + public $id; + /** + * @var Controller the controller that owns this action + */ + public $controller; - /** - * Constructor. - * @param string $id the ID of this action - * @param Controller $controller the controller that owns this action - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $controller, $config = []) - { - $this->id = $id; - $this->controller = $controller; - parent::__construct($config); - } + /** + * Constructor. + * @param string $id the ID of this action + * @param Controller $controller the controller that owns this action + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $controller, $config = []) + { + $this->id = $id; + $this->controller = $controller; + parent::__construct($config); + } - /** - * Returns the unique ID of this action among the whole application. - * @return string the unique ID of this action among the whole application. - */ - public function getUniqueId() - { - return $this->controller->getUniqueId() . '/' . $this->id; - } + /** + * Returns the unique ID of this action among the whole application. + * @return string the unique ID of this action among the whole application. + */ + public function getUniqueId() + { + return $this->controller->getUniqueId() . '/' . $this->id; + } - /** - * Runs this action with the specified parameters. - * This method is mainly invoked by the controller. - * @param array $params the parameters to be bound to the action's run() method. - * @return mixed the result of the action - * @throws InvalidConfigException if the action class does not have a run() method - */ - public function runWithParams($params) - { - if (!method_exists($this, 'run')) { - throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); - } - $args = $this->controller->bindActionParams($this, $params); - Yii::trace('Running action: ' . get_class($this) . '::run()', __METHOD__); - if (Yii::$app->requestedParams === null) { - Yii::$app->requestedParams = $args; - } - if ($this->beforeRun()) { - $result = call_user_func_array([$this, 'run'], $args); - $this->afterRun(); - return $result; - } else { - return null; - } - } + /** + * Runs this action with the specified parameters. + * This method is mainly invoked by the controller. + * @param array $params the parameters to be bound to the action's run() method. + * @return mixed the result of the action + * @throws InvalidConfigException if the action class does not have a run() method + */ + public function runWithParams($params) + { + if (!method_exists($this, 'run')) { + throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); + } + $args = $this->controller->bindActionParams($this, $params); + Yii::trace('Running action: ' . get_class($this) . '::run()', __METHOD__); + if (Yii::$app->requestedParams === null) { + Yii::$app->requestedParams = $args; + } + if ($this->beforeRun()) { + $result = call_user_func_array([$this, 'run'], $args); + $this->afterRun(); - /** - * This method is called right before `run()` is executed. - * You may override this method to do preparation work for the action run. - * If the method returns false, it will cancel the action. - * @return boolean whether to run the action. - */ - protected function beforeRun() - { - return true; - } + return $result; + } else { + return null; + } + } - /** - * This method is called right after `run()` is executed. - * You may override this method to do post-processing work for the action run. - */ - protected function afterRun() - { - } + /** + * This method is called right before `run()` is executed. + * You may override this method to do preparation work for the action run. + * If the method returns false, it will cancel the action. + * @return boolean whether to run the action. + */ + protected function beforeRun() + { + return true; + } + + /** + * This method is called right after `run()` is executed. + * You may override this method to do post-processing work for the action run. + */ + protected function afterRun() + { + } } diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php index 6e123a009e9..7c76b918c92 100644 --- a/framework/base/ActionEvent.php +++ b/framework/base/ActionEvent.php @@ -17,29 +17,29 @@ */ class ActionEvent extends Event { - /** - * @var Action the action currently being executed - */ - public $action; - /** - * @var mixed the action result. Event handlers may modify this property to change the action result. - */ - public $result; - /** - * @var boolean whether to continue running the action. Event handlers of - * [[Controller::EVENT_BEFORE_ACTION]] may set this property to decide whether - * to continue running the current action. - */ - public $isValid = true; + /** + * @var Action the action currently being executed + */ + public $action; + /** + * @var mixed the action result. Event handlers may modify this property to change the action result. + */ + public $result; + /** + * @var boolean whether to continue running the action. Event handlers of + * [[Controller::EVENT_BEFORE_ACTION]] may set this property to decide whether + * to continue running the current action. + */ + public $isValid = true; - /** - * Constructor. - * @param Action $action the action associated with this action event. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($action, $config = []) - { - $this->action = $action; - parent::__construct($config); - } + /** + * Constructor. + * @param Action $action the action associated with this action event. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($action, $config = []) + { + $this->action = $action; + parent::__construct($config); + } } diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 6fb114ca037..4bb2cf441cb 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -18,87 +18,88 @@ */ class ActionFilter extends Behavior { - /** - * @var array list of action IDs that this filter should apply to. If this property is not set, - * then the filter applies to all actions, unless they are listed in [[except]]. - * If an action ID appears in both [[only]] and [[except]], this filter will NOT apply to it. - * @see except - */ - public $only; - /** - * @var array list of action IDs that this filter should not apply to. - * @see only - */ - public $except = []; + /** + * @var array list of action IDs that this filter should apply to. If this property is not set, + * then the filter applies to all actions, unless they are listed in [[except]]. + * If an action ID appears in both [[only]] and [[except]], this filter will NOT apply to it. + * @see except + */ + public $only; + /** + * @var array list of action IDs that this filter should not apply to. + * @see only + */ + public $except = []; - /** - * Declares event handlers for the [[owner]]'s events. - * @return array events (array keys) and the corresponding event handler methods (array values). - */ - public function events() - { - return [ - Controller::EVENT_BEFORE_ACTION => 'beforeFilter', - Controller::EVENT_AFTER_ACTION => 'afterFilter', - ]; - } + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return [ + Controller::EVENT_BEFORE_ACTION => 'beforeFilter', + Controller::EVENT_AFTER_ACTION => 'afterFilter', + ]; + } - /** - * @param ActionEvent $event - * @return boolean - */ - public function beforeFilter($event) - { - if ($this->isActive($event->action)) { - $event->isValid = $this->beforeAction($event->action); - if (!$event->isValid) { - $event->handled = true; - } - } - return $event->isValid; - } + /** + * @param ActionEvent $event + * @return boolean + */ + public function beforeFilter($event) + { + if ($this->isActive($event->action)) { + $event->isValid = $this->beforeAction($event->action); + if (!$event->isValid) { + $event->handled = true; + } + } - /** - * @param ActionEvent $event - * @return boolean - */ - public function afterFilter($event) - { - if ($this->isActive($event->action)) { - $event->result = $this->afterAction($event->action, $event->result); - } - } + return $event->isValid; + } - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - return true; - } + /** + * @param ActionEvent $event + * @return boolean + */ + public function afterFilter($event) + { + if ($this->isActive($event->action)) { + $event->result = $this->afterAction($event->action, $event->result); + } + } - /** - * This method is invoked right after an action is executed. - * You may override this method to do some postprocessing for the action. - * @param Action $action the action just executed. - * @param mixed $result the action execution result - * @return mixed the processed action result. - */ - public function afterAction($action, $result) - { - return $result; - } + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + return true; + } - /** - * Returns a value indicating whether the filer is active for the given action. - * @param Action $action the action being filtered - * @return boolean whether the filer is active for the given action. - */ - protected function isActive($action) - { - return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); - } + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + * @param mixed $result the action execution result + * @return mixed the processed action result. + */ + public function afterAction($action, $result) + { + return $result; + } + + /** + * Returns a value indicating whether the filer is active for the given action. + * @param Action $action the action being filtered + * @return boolean whether the filer is active for the given action. + */ + protected function isActive($action) + { + return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); + } } diff --git a/framework/base/Application.php b/framework/base/Application.php index ffdc4a7b7bf..33efe2c657d 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -40,610 +40,612 @@ */ abstract class Application extends Module { - /** - * @event Event an event raised before the application starts to handle a request. - */ - const EVENT_BEFORE_REQUEST = 'beforeRequest'; - /** - * @event Event an event raised after the application successfully handles a request (before the response is sent out). - */ - const EVENT_AFTER_REQUEST = 'afterRequest'; - /** - * @event ActionEvent an event raised before executing a controller action. - * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. - */ - const EVENT_BEFORE_ACTION = 'beforeAction'; - /** - * @event ActionEvent an event raised after executing a controller action. - */ - const EVENT_AFTER_ACTION = 'afterAction'; - - /** - * @var string the namespace that controller classes are in. If not set, - * it will use the "app\controllers" namespace. - */ - public $controllerNamespace = 'app\\controllers'; - - /** - * @var string the application name. - */ - public $name = 'My Application'; - /** - * @var string the version of this application. - */ - public $version = '1.0'; - /** - * @var string the charset currently used for the application. - */ - public $charset = 'UTF-8'; - /** - * @var string the language that is meant to be used for end users. - * @see sourceLanguage - */ - public $language = 'en'; - /** - * @var string the language that the application is written in. This mainly refers to - * the language that the messages and view files are written in. - * @see language - */ - public $sourceLanguage = 'en'; - /** - * @var Controller the currently active controller instance - */ - public $controller; - /** - * @var string|boolean the layout that should be applied for views in this application. Defaults to 'main'. - * If this is false, layout will be disabled. - */ - public $layout = 'main'; - /** - * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that - * when an out-of-memory issue occurs, the error handler is able to handle the error with - * the help of this reserved memory. If you set this value to be 0, no memory will be reserved. - * Defaults to 256KB. - */ - public $memoryReserveSize = 262144; - /** - * @var string the requested route - */ - public $requestedRoute; - /** - * @var Action the requested Action. If null, it means the request cannot be resolved into an action. - */ - public $requestedAction; - /** - * @var array the parameters supplied to the requested action. - */ - public $requestedParams; - /** - * @var array list of installed Yii extensions. Each array element represents a single extension - * with the following structure: - * - * ~~~ - * [ - * 'name' => 'extension name', - * 'version' => 'version number', - * 'bootstrap' => 'BootstrapClassName', - * ] - * ~~~ - */ - public $extensions = []; - /** - * @var \Exception the exception that is being handled currently. When this is not null, - * it means the application is handling some exception and extra care should be taken. - */ - public $exception; - - /** - * @var string Used to reserve memory for fatal error handler. - */ - private $_memoryReserve; - - /** - * Constructor. - * @param array $config name-value pairs that will be used to initialize the object properties. - * Note that the configuration must contain both [[id]] and [[basePath]]. - * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing. - */ - public function __construct($config = []) - { - Yii::$app = $this; - - $this->preInit($config); - $this->registerErrorHandlers(); - $this->registerCoreComponents(); - - Component::__construct($config); - } - - /** - * Pre-initializes the application. - * This method is called at the beginning of the application constructor. - * It initializes several important application properties. - * If you override this method, please make sure you call the parent implementation. - * @param array $config the application configuration - * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing. - */ - public function preInit(&$config) - { - if (!isset($config['id'])) { - throw new InvalidConfigException('The "id" configuration is required.'); - } - if (isset($config['basePath'])) { - $this->setBasePath($config['basePath']); - unset($config['basePath']); - } else { - throw new InvalidConfigException('The "basePath" configuration is required.'); - } - - if (isset($config['vendorPath'])) { - $this->setVendorPath($config['vendorPath']); - unset($config['vendorPath']); - } else { - // set "@vendor" - $this->getVendorPath(); - } - if (isset($config['runtimePath'])) { - $this->setRuntimePath($config['runtimePath']); - unset($config['runtimePath']); - } else { - // set "@runtime" - $this->getRuntimePath(); - } - - if (isset($config['timeZone'])) { - $this->setTimeZone($config['timeZone']); - unset($config['timeZone']); - } elseif (!ini_get('date.timezone')) { - $this->setTimeZone('UTC'); - } - } - - /** - * @inheritdoc - */ - public function init() - { - $this->initExtensions($this->extensions); - parent::init(); - } - - /** - * Initializes the extensions. - * @param array $extensions the extensions to be initialized. Please refer to [[extensions]] - * for the structure of the extension array. - */ - protected function initExtensions($extensions) - { - foreach ($extensions as $extension) { - if (!empty($extension['alias'])) { - foreach ($extension['alias'] as $name => $path) { - Yii::setAlias($name, $path); - } - } - if (isset($extension['bootstrap'])) { - /** @var Extension $class */ - $class = $extension['bootstrap']; - $class::init(); - } - } - } - - /** - * Loads components that are declared in [[preload]]. - * @throws InvalidConfigException if a component or module to be preloaded is unknown - */ - public function preloadComponents() - { - $this->getComponent('log'); - parent::preloadComponents(); - } - - /** - * Registers error handlers. - */ - public function registerErrorHandlers() - { - if (YII_ENABLE_ERROR_HANDLER) { - ini_set('display_errors', 0); - set_exception_handler([$this, 'handleException']); - set_error_handler([$this, 'handleError']); - if ($this->memoryReserveSize > 0) { - $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); - } - register_shutdown_function([$this, 'handleFatalError']); - } - } - - /** - * Returns an ID that uniquely identifies this module among all modules within the current application. - * Since this is an application instance, it will always return an empty string. - * @return string the unique ID of the module. - */ - public function getUniqueId() - { - return ''; - } - - /** - * Sets the root directory of the application and the @app alias. - * This method can only be invoked at the beginning of the constructor. - * @param string $path the root directory of the application. - * @property string the root directory of the application. - * @throws InvalidParamException if the directory does not exist. - */ - public function setBasePath($path) - { - parent::setBasePath($path); - Yii::setAlias('@app', $this->getBasePath()); - } - - /** - * Runs the application. - * This is the main entrance of an application. - * @return integer the exit status (0 means normal, non-zero values mean abnormal) - */ - public function run() - { - $this->trigger(self::EVENT_BEFORE_REQUEST); - $response = $this->handleRequest($this->getRequest()); - $this->trigger(self::EVENT_AFTER_REQUEST); - $response->send(); - return $response->exitStatus; - } - - /** - * Handles the specified request. - * - * This method should return an instance of [[Response]] or its child class - * which represents the handling result of the request. - * - * @param Request $request the request to be handled - * @return Response the resulting response - */ - abstract public function handleRequest($request); - - - private $_runtimePath; - - /** - * Returns the directory that stores runtime files. - * @return string the directory that stores runtime files. - * Defaults to the "runtime" subdirectory under [[basePath]]. - */ - public function getRuntimePath() - { - if ($this->_runtimePath === null) { - $this->setRuntimePath($this->getBasePath() . DIRECTORY_SEPARATOR . 'runtime'); - } - return $this->_runtimePath; - } - - /** - * Sets the directory that stores runtime files. - * @param string $path the directory that stores runtime files. - */ - public function setRuntimePath($path) - { - $this->_runtimePath = Yii::getAlias($path); - Yii::setAlias('@runtime', $this->_runtimePath); - } - - private $_vendorPath; - - /** - * Returns the directory that stores vendor files. - * @return string the directory that stores vendor files. - * Defaults to "vendor" directory under [[basePath]]. - */ - public function getVendorPath() - { - if ($this->_vendorPath === null) { - $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor'); - } - return $this->_vendorPath; - } - - /** - * Sets the directory that stores vendor files. - * @param string $path the directory that stores vendor files. - */ - public function setVendorPath($path) - { - $this->_vendorPath = Yii::getAlias($path); - Yii::setAlias('@vendor', $this->_vendorPath); - } - - /** - * Returns the time zone used by this application. - * This is a simple wrapper of PHP function date_default_timezone_get(). - * If time zone is not configured in php.ini or application config, - * it will be set to UTC by default. - * @return string the time zone used by this application. - * @see http://php.net/manual/en/function.date-default-timezone-get.php - */ - public function getTimeZone() - { - return date_default_timezone_get(); - } - - /** - * Sets the time zone used by this application. - * This is a simple wrapper of PHP function date_default_timezone_set(). - * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones. - * @param string $value the time zone used by this application. - * @see http://php.net/manual/en/function.date-default-timezone-set.php - */ - public function setTimeZone($value) - { - date_default_timezone_set($value); - } - - /** - * Returns the database connection component. - * @return \yii\db\Connection the database connection - */ - public function getDb() - { - return $this->getComponent('db'); - } - - /** - * Returns the log component. - * @return \yii\log\Logger the log component - */ - public function getLog() - { - return $this->getComponent('log'); - } - - /** - * Returns the error handler component. - * @return ErrorHandler the error handler application component. - */ - public function getErrorHandler() - { - return $this->getComponent('errorHandler'); - } - - /** - * Returns the cache component. - * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. - */ - public function getCache() - { - return $this->getComponent('cache'); - } - - /** - * Returns the formatter component. - * @return \yii\base\Formatter the formatter application component. - */ - public function getFormatter() - { - return $this->getComponent('formatter'); - } - - /** - * Returns the request component. - * @return \yii\web\Request|\yii\console\Request the request component - */ - public function getRequest() - { - return $this->getComponent('request'); - } - - /** - * Returns the view object. - * @return View|\yii\web\View the view object that is used to render various view files. - */ - public function getView() - { - return $this->getComponent('view'); - } - - /** - * Returns the URL manager for this application. - * @return \yii\web\UrlManager the URL manager for this application. - */ - public function getUrlManager() - { - return $this->getComponent('urlManager'); - } - - /** - * Returns the internationalization (i18n) component - * @return \yii\i18n\I18N the internationalization component - */ - public function getI18n() - { - return $this->getComponent('i18n'); - } - - /** - * Returns the mailer component. - * @return \yii\mail\MailerInterface the mailer interface - */ - public function getMail() - { - return $this->getComponent('mail'); - } - - /** - * Returns the auth manager for this application. - * @return \yii\rbac\Manager the auth manager for this application. - */ - public function getAuthManager() - { - return $this->getComponent('authManager'); - } - - /** - * Registers the core application components. - * @see setComponents - */ - public function registerCoreComponents() - { - $this->setComponents([ - 'log' => ['class' => 'yii\log\Logger'], - 'errorHandler' => ['class' => 'yii\base\ErrorHandler'], - 'formatter' => ['class' => 'yii\base\Formatter'], - 'i18n' => ['class' => 'yii\i18n\I18N'], - 'mail' => ['class' => 'yii\swiftmailer\Mailer'], - 'urlManager' => ['class' => 'yii\web\UrlManager'], - 'view' => ['class' => 'yii\web\View'], - ]); - } - - /** - * Handles uncaught PHP exceptions. - * - * This method is implemented as a PHP exception handler. - * - * @param \Exception $exception the exception that is not caught - */ - public function handleException($exception) - { - $this->exception = $exception; - - // disable error capturing to avoid recursive errors while handling exceptions - restore_error_handler(); - restore_exception_handler(); - try { - $this->logException($exception); - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - echo $this->renderException($exception); - if (PHP_SAPI === 'cli' && !YII_ENV_TEST) { - exit(1); - } - } - } catch (\Exception $e) { - // exception could be thrown in ErrorHandler::handle() - $msg = (string)$e; - $msg .= "\nPrevious exception:\n"; - $msg .= (string)$exception; - if (YII_DEBUG) { - if (PHP_SAPI === 'cli') { - echo $msg . "\n"; - } else { - echo '
    ' . htmlspecialchars($msg, ENT_QUOTES, $this->charset) . '
    '; - } - } - $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); - error_log($msg); - exit(1); - } - } - - /** - * Handles PHP execution errors such as warnings, notices. - * - * This method is used as a PHP error handler. It will simply raise an `ErrorException`. - * - * @param integer $code the level of the error raised - * @param string $message the error message - * @param string $file the filename that the error was raised in - * @param integer $line the line number the error was raised at - * - * @throws ErrorException - */ - public function handleError($code, $message, $file, $line) - { - if (error_reporting() & $code) { - // load ErrorException manually here because autoloading them will not work - // when error occurs while autoloading a class - if (!class_exists('\\yii\\base\\Exception', false)) { - require_once(__DIR__ . '/Exception.php'); - } - if (!class_exists('\\yii\\base\\ErrorException', false)) { - require_once(__DIR__ . '/ErrorException.php'); - } - $exception = new ErrorException($message, $code, $code, $file, $line); - - // in case error appeared in __toString method we can't throw any exception - $trace = debug_backtrace(false); - array_shift($trace); - foreach ($trace as $frame) { - if ($frame['function'] == '__toString') { - $this->handleException($exception); - exit(1); - } - } - - throw $exception; - } - } - - /** - * Handles fatal PHP errors - */ - public function handleFatalError() - { - unset($this->_memoryReserve); - - // load ErrorException manually here because autoloading them will not work - // when error occurs while autoloading a class - if (!class_exists('\\yii\\base\\Exception', false)) { - require_once(__DIR__ . '/Exception.php'); - } - if (!class_exists('\\yii\\base\\ErrorException', false)) { - require_once(__DIR__ . '/ErrorException.php'); - } - - $error = error_get_last(); - - if (ErrorException::isFatalError($error)) { - $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); - $this->exception = $exception; - // use error_log because it's too late to use Yii log - error_log($exception); - - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - echo $this->renderException($exception); - } - - exit(1); - } - } - - /** - * Renders an exception without using rich format. - * @param \Exception $exception the exception to be rendered. - * @return string the rendering result - */ - public function renderException($exception) - { - if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { - $message = $exception->getName() . ': ' . $exception->getMessage(); - if (Yii::$app->controller instanceof \yii\console\Controller) { - $message = Yii::$app->controller->ansiFormat($message, Console::FG_RED); - } - } else { - $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage(); - } - if (PHP_SAPI === 'cli') { - return $message . "\n"; - } else { - return '
    ' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '
    '; - } - } - - /** - * Logs the given exception - * @param \Exception $exception the exception to be logged - */ - protected function logException($exception) - { - $category = get_class($exception); - if ($exception instanceof HttpException) { - $category = 'yii\\web\\HttpException:' . $exception->statusCode; - } elseif ($exception instanceof \ErrorException) { - $category .= ':' . $exception->getSeverity(); - } - Yii::error((string)$exception, $category); - } + /** + * @event Event an event raised before the application starts to handle a request. + */ + const EVENT_BEFORE_REQUEST = 'beforeRequest'; + /** + * @event Event an event raised after the application successfully handles a request (before the response is sent out). + */ + const EVENT_AFTER_REQUEST = 'afterRequest'; + /** + * @event ActionEvent an event raised before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + + /** + * @var string the namespace that controller classes are in. If not set, + * it will use the "app\controllers" namespace. + */ + public $controllerNamespace = 'app\\controllers'; + + /** + * @var string the application name. + */ + public $name = 'My Application'; + /** + * @var string the version of this application. + */ + public $version = '1.0'; + /** + * @var string the charset currently used for the application. + */ + public $charset = 'UTF-8'; + /** + * @var string the language that is meant to be used for end users. + * @see sourceLanguage + */ + public $language = 'en'; + /** + * @var string the language that the application is written in. This mainly refers to + * the language that the messages and view files are written in. + * @see language + */ + public $sourceLanguage = 'en'; + /** + * @var Controller the currently active controller instance + */ + public $controller; + /** + * @var string|boolean the layout that should be applied for views in this application. Defaults to 'main'. + * If this is false, layout will be disabled. + */ + public $layout = 'main'; + /** + * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that + * when an out-of-memory issue occurs, the error handler is able to handle the error with + * the help of this reserved memory. If you set this value to be 0, no memory will be reserved. + * Defaults to 256KB. + */ + public $memoryReserveSize = 262144; + /** + * @var string the requested route + */ + public $requestedRoute; + /** + * @var Action the requested Action. If null, it means the request cannot be resolved into an action. + */ + public $requestedAction; + /** + * @var array the parameters supplied to the requested action. + */ + public $requestedParams; + /** + * @var array list of installed Yii extensions. Each array element represents a single extension + * with the following structure: + * + * ~~~ + * [ + * 'name' => 'extension name', + * 'version' => 'version number', + * 'bootstrap' => 'BootstrapClassName', + * ] + * ~~~ + */ + public $extensions = []; + /** + * @var \Exception the exception that is being handled currently. When this is not null, + * it means the application is handling some exception and extra care should be taken. + */ + public $exception; + + /** + * @var string Used to reserve memory for fatal error handler. + */ + private $_memoryReserve; + + /** + * Constructor. + * @param array $config name-value pairs that will be used to initialize the object properties. + * Note that the configuration must contain both [[id]] and [[basePath]]. + * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing. + */ + public function __construct($config = []) + { + Yii::$app = $this; + + $this->preInit($config); + $this->registerErrorHandlers(); + $this->registerCoreComponents(); + + Component::__construct($config); + } + + /** + * Pre-initializes the application. + * This method is called at the beginning of the application constructor. + * It initializes several important application properties. + * If you override this method, please make sure you call the parent implementation. + * @param array $config the application configuration + * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing. + */ + public function preInit(&$config) + { + if (!isset($config['id'])) { + throw new InvalidConfigException('The "id" configuration is required.'); + } + if (isset($config['basePath'])) { + $this->setBasePath($config['basePath']); + unset($config['basePath']); + } else { + throw new InvalidConfigException('The "basePath" configuration is required.'); + } + + if (isset($config['vendorPath'])) { + $this->setVendorPath($config['vendorPath']); + unset($config['vendorPath']); + } else { + // set "@vendor" + $this->getVendorPath(); + } + if (isset($config['runtimePath'])) { + $this->setRuntimePath($config['runtimePath']); + unset($config['runtimePath']); + } else { + // set "@runtime" + $this->getRuntimePath(); + } + + if (isset($config['timeZone'])) { + $this->setTimeZone($config['timeZone']); + unset($config['timeZone']); + } elseif (!ini_get('date.timezone')) { + $this->setTimeZone('UTC'); + } + } + + /** + * @inheritdoc + */ + public function init() + { + $this->initExtensions($this->extensions); + parent::init(); + } + + /** + * Initializes the extensions. + * @param array $extensions the extensions to be initialized. Please refer to [[extensions]] + * for the structure of the extension array. + */ + protected function initExtensions($extensions) + { + foreach ($extensions as $extension) { + if (!empty($extension['alias'])) { + foreach ($extension['alias'] as $name => $path) { + Yii::setAlias($name, $path); + } + } + if (isset($extension['bootstrap'])) { + /** @var Extension $class */ + $class = $extension['bootstrap']; + $class::init(); + } + } + } + + /** + * Loads components that are declared in [[preload]]. + * @throws InvalidConfigException if a component or module to be preloaded is unknown + */ + public function preloadComponents() + { + $this->getComponent('log'); + parent::preloadComponents(); + } + + /** + * Registers error handlers. + */ + public function registerErrorHandlers() + { + if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); + set_exception_handler([$this, 'handleException']); + set_error_handler([$this, 'handleError']); + if ($this->memoryReserveSize > 0) { + $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); + } + register_shutdown_function([$this, 'handleFatalError']); + } + } + + /** + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Since this is an application instance, it will always return an empty string. + * @return string the unique ID of the module. + */ + public function getUniqueId() + { + return ''; + } + + /** + * Sets the root directory of the application and the @app alias. + * This method can only be invoked at the beginning of the constructor. + * @param string $path the root directory of the application. + * @property string the root directory of the application. + * @throws InvalidParamException if the directory does not exist. + */ + public function setBasePath($path) + { + parent::setBasePath($path); + Yii::setAlias('@app', $this->getBasePath()); + } + + /** + * Runs the application. + * This is the main entrance of an application. + * @return integer the exit status (0 means normal, non-zero values mean abnormal) + */ + public function run() + { + $this->trigger(self::EVENT_BEFORE_REQUEST); + $response = $this->handleRequest($this->getRequest()); + $this->trigger(self::EVENT_AFTER_REQUEST); + $response->send(); + + return $response->exitStatus; + } + + /** + * Handles the specified request. + * + * This method should return an instance of [[Response]] or its child class + * which represents the handling result of the request. + * + * @param Request $request the request to be handled + * @return Response the resulting response + */ + abstract public function handleRequest($request); + + private $_runtimePath; + + /** + * Returns the directory that stores runtime files. + * @return string the directory that stores runtime files. + * Defaults to the "runtime" subdirectory under [[basePath]]. + */ + public function getRuntimePath() + { + if ($this->_runtimePath === null) { + $this->setRuntimePath($this->getBasePath() . DIRECTORY_SEPARATOR . 'runtime'); + } + + return $this->_runtimePath; + } + + /** + * Sets the directory that stores runtime files. + * @param string $path the directory that stores runtime files. + */ + public function setRuntimePath($path) + { + $this->_runtimePath = Yii::getAlias($path); + Yii::setAlias('@runtime', $this->_runtimePath); + } + + private $_vendorPath; + + /** + * Returns the directory that stores vendor files. + * @return string the directory that stores vendor files. + * Defaults to "vendor" directory under [[basePath]]. + */ + public function getVendorPath() + { + if ($this->_vendorPath === null) { + $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor'); + } + + return $this->_vendorPath; + } + + /** + * Sets the directory that stores vendor files. + * @param string $path the directory that stores vendor files. + */ + public function setVendorPath($path) + { + $this->_vendorPath = Yii::getAlias($path); + Yii::setAlias('@vendor', $this->_vendorPath); + } + + /** + * Returns the time zone used by this application. + * This is a simple wrapper of PHP function date_default_timezone_get(). + * If time zone is not configured in php.ini or application config, + * it will be set to UTC by default. + * @return string the time zone used by this application. + * @see http://php.net/manual/en/function.date-default-timezone-get.php + */ + public function getTimeZone() + { + return date_default_timezone_get(); + } + + /** + * Sets the time zone used by this application. + * This is a simple wrapper of PHP function date_default_timezone_set(). + * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones. + * @param string $value the time zone used by this application. + * @see http://php.net/manual/en/function.date-default-timezone-set.php + */ + public function setTimeZone($value) + { + date_default_timezone_set($value); + } + + /** + * Returns the database connection component. + * @return \yii\db\Connection the database connection + */ + public function getDb() + { + return $this->getComponent('db'); + } + + /** + * Returns the log component. + * @return \yii\log\Logger the log component + */ + public function getLog() + { + return $this->getComponent('log'); + } + + /** + * Returns the error handler component. + * @return ErrorHandler the error handler application component. + */ + public function getErrorHandler() + { + return $this->getComponent('errorHandler'); + } + + /** + * Returns the cache component. + * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. + */ + public function getCache() + { + return $this->getComponent('cache'); + } + + /** + * Returns the formatter component. + * @return \yii\base\Formatter the formatter application component. + */ + public function getFormatter() + { + return $this->getComponent('formatter'); + } + + /** + * Returns the request component. + * @return \yii\web\Request|\yii\console\Request the request component + */ + public function getRequest() + { + return $this->getComponent('request'); + } + + /** + * Returns the view object. + * @return View|\yii\web\View the view object that is used to render various view files. + */ + public function getView() + { + return $this->getComponent('view'); + } + + /** + * Returns the URL manager for this application. + * @return \yii\web\UrlManager the URL manager for this application. + */ + public function getUrlManager() + { + return $this->getComponent('urlManager'); + } + + /** + * Returns the internationalization (i18n) component + * @return \yii\i18n\I18N the internationalization component + */ + public function getI18n() + { + return $this->getComponent('i18n'); + } + + /** + * Returns the mailer component. + * @return \yii\mail\MailerInterface the mailer interface + */ + public function getMail() + { + return $this->getComponent('mail'); + } + + /** + * Returns the auth manager for this application. + * @return \yii\rbac\Manager the auth manager for this application. + */ + public function getAuthManager() + { + return $this->getComponent('authManager'); + } + + /** + * Registers the core application components. + * @see setComponents + */ + public function registerCoreComponents() + { + $this->setComponents([ + 'log' => ['class' => 'yii\log\Logger'], + 'errorHandler' => ['class' => 'yii\base\ErrorHandler'], + 'formatter' => ['class' => 'yii\base\Formatter'], + 'i18n' => ['class' => 'yii\i18n\I18N'], + 'mail' => ['class' => 'yii\swiftmailer\Mailer'], + 'urlManager' => ['class' => 'yii\web\UrlManager'], + 'view' => ['class' => 'yii\web\View'], + ]); + } + + /** + * Handles uncaught PHP exceptions. + * + * This method is implemented as a PHP exception handler. + * + * @param \Exception $exception the exception that is not caught + */ + public function handleException($exception) + { + $this->exception = $exception; + + // disable error capturing to avoid recursive errors while handling exceptions + restore_error_handler(); + restore_exception_handler(); + try { + $this->logException($exception); + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + echo $this->renderException($exception); + if (PHP_SAPI === 'cli' && !YII_ENV_TEST) { + exit(1); + } + } + } catch (\Exception $e) { + // exception could be thrown in ErrorHandler::handle() + $msg = (string) $e; + $msg .= "\nPrevious exception:\n"; + $msg .= (string) $exception; + if (YII_DEBUG) { + if (PHP_SAPI === 'cli') { + echo $msg . "\n"; + } else { + echo '
    ' . htmlspecialchars($msg, ENT_QUOTES, $this->charset) . '
    '; + } + } + $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); + error_log($msg); + exit(1); + } + } + + /** + * Handles PHP execution errors such as warnings, notices. + * + * This method is used as a PHP error handler. It will simply raise an `ErrorException`. + * + * @param integer $code the level of the error raised + * @param string $message the error message + * @param string $file the filename that the error was raised in + * @param integer $line the line number the error was raised at + * + * @throws ErrorException + */ + public function handleError($code, $message, $file, $line) + { + if (error_reporting() & $code) { + // load ErrorException manually here because autoloading them will not work + // when error occurs while autoloading a class + if (!class_exists('\\yii\\base\\Exception', false)) { + require_once(__DIR__ . '/Exception.php'); + } + if (!class_exists('\\yii\\base\\ErrorException', false)) { + require_once(__DIR__ . '/ErrorException.php'); + } + $exception = new ErrorException($message, $code, $code, $file, $line); + + // in case error appeared in __toString method we can't throw any exception + $trace = debug_backtrace(false); + array_shift($trace); + foreach ($trace as $frame) { + if ($frame['function'] == '__toString') { + $this->handleException($exception); + exit(1); + } + } + + throw $exception; + } + } + + /** + * Handles fatal PHP errors + */ + public function handleFatalError() + { + unset($this->_memoryReserve); + + // load ErrorException manually here because autoloading them will not work + // when error occurs while autoloading a class + if (!class_exists('\\yii\\base\\Exception', false)) { + require_once(__DIR__ . '/Exception.php'); + } + if (!class_exists('\\yii\\base\\ErrorException', false)) { + require_once(__DIR__ . '/ErrorException.php'); + } + + $error = error_get_last(); + + if (ErrorException::isFatalError($error)) { + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + $this->exception = $exception; + // use error_log because it's too late to use Yii log + error_log($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + echo $this->renderException($exception); + } + + exit(1); + } + } + + /** + * Renders an exception without using rich format. + * @param \Exception $exception the exception to be rendered. + * @return string the rendering result + */ + public function renderException($exception) + { + if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { + $message = $exception->getName() . ': ' . $exception->getMessage(); + if (Yii::$app->controller instanceof \yii\console\Controller) { + $message = Yii::$app->controller->ansiFormat($message, Console::FG_RED); + } + } else { + $message = YII_DEBUG ? (string) $exception : 'Error: ' . $exception->getMessage(); + } + if (PHP_SAPI === 'cli') { + return $message . "\n"; + } else { + return '
    ' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '
    '; + } + } + + /** + * Logs the given exception + * @param \Exception $exception the exception to be logged + */ + protected function logException($exception) + { + $category = get_class($exception); + if ($exception instanceof HttpException) { + $category = 'yii\\web\\HttpException:' . $exception->statusCode; + } elseif ($exception instanceof \ErrorException) { + $category .= ':' . $exception->getSeverity(); + } + Yii::error((string) $exception, $category); + } } diff --git a/framework/base/ArrayAccessTrait.php b/framework/base/ArrayAccessTrait.php index 3ccbd975d2f..43b7fff1736 100644 --- a/framework/base/ArrayAccessTrait.php +++ b/framework/base/ArrayAccessTrait.php @@ -18,63 +18,63 @@ */ trait ArrayAccessTrait { - /** - * Returns an iterator for traversing the data. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the collection. - * @return \ArrayIterator an iterator for traversing the cookies in the collection. - */ - public function getIterator() - { - return new \ArrayIterator($this->data); - } + /** + * Returns an iterator for traversing the data. + * This method is required by the SPL interface `IteratorAggregate`. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return \ArrayIterator an iterator for traversing the cookies in the collection. + */ + public function getIterator() + { + return new \ArrayIterator($this->data); + } - /** - * Returns the number of data items. - * This method is required by Countable interface. - * @return integer number of data elements. - */ - public function count() - { - return count($this->data); - } + /** + * Returns the number of data items. + * This method is required by Countable interface. + * @return integer number of data elements. + */ + public function count() + { + return count($this->data); + } - /** - * This method is required by the interface ArrayAccess. - * @param mixed $offset the offset to check on - * @return boolean - */ - public function offsetExists($offset) - { - return isset($this->data[$offset]); - } + /** + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean + */ + public function offsetExists($offset) + { + return isset($this->data[$offset]); + } - /** - * This method is required by the interface ArrayAccess. - * @param integer $offset the offset to retrieve element. - * @return mixed the element at the offset, null if no element is found at the offset - */ - public function offsetGet($offset) - { - return isset($this->data[$offset]) ? $this->data[$offset] : null; - } + /** + * This method is required by the interface ArrayAccess. + * @param integer $offset the offset to retrieve element. + * @return mixed the element at the offset, null if no element is found at the offset + */ + public function offsetGet($offset) + { + return isset($this->data[$offset]) ? $this->data[$offset] : null; + } - /** - * This method is required by the interface ArrayAccess. - * @param integer $offset the offset to set element - * @param mixed $item the element value - */ - public function offsetSet($offset, $item) - { - $this->data[$offset] = $item; - } + /** + * This method is required by the interface ArrayAccess. + * @param integer $offset the offset to set element + * @param mixed $item the element value + */ + public function offsetSet($offset, $item) + { + $this->data[$offset] = $item; + } - /** - * This method is required by the interface ArrayAccess. - * @param mixed $offset the offset to unset element - */ - public function offsetUnset($offset) - { - unset($this->data[$offset]); - } + /** + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to unset element + */ + public function offsetUnset($offset) + { + unset($this->data[$offset]); + } } diff --git a/framework/base/Arrayable.php b/framework/base/Arrayable.php index 694424576fa..4a8fd45d8e4 100644 --- a/framework/base/Arrayable.php +++ b/framework/base/Arrayable.php @@ -22,69 +22,69 @@ */ interface Arrayable { - /** - * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. - * - * A field is a named element in the returned array by [[toArray()]]. - * - * This method should return an array of field names or field definitions. - * If the former, the field name will be treated as an object property name whose value will be used - * as the field value. If the latter, the array key should be the field name while the array value should be - * the corresponding field definition which can be either an object property name or a PHP callable - * returning the corresponding field value. The signature of the callable should be: - * - * ```php - * function ($field, $model) { - * // return field value - * } - * ``` - * - * For example, the following code declares four fields: - * - * - `email`: the field name is the same as the property name `email`; - * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their - * values are obtained from the `first_name` and `last_name` properties; - * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` - * and `last_name`. - * - * ```php - * return [ - * 'email', - * 'firstName' => 'first_name', - * 'lastName' => 'last_name', - * 'fullName' => function () { - * return $this->first_name . ' ' . $this->last_name; - * }, - * ]; - * ``` - * - * @return array the list of field names or field definitions. - * @see toArray() - */ - public function fields(); - /** - * Returns the list of additional fields that can be returned by [[toArray()]] in addition to those listed in [[fields()]]. - * - * This method is similar to [[fields()]] except that the list of fields declared - * by this method are not returned by default by [[toArray()]]. Only when a field in the list - * is explicitly requested, will it be included in the result of [[toArray()]]. - * - * @return array the list of expandable field names or field definitions. Please refer - * to [[fields()]] on the format of the return value. - * @see toArray() - * @see fields() - */ - public function extraFields(); - /** - * Converts the object into an array. - * - * @param array $fields the fields that the output array should contain. Fields not specified - * in [[fields()]] will be ignored. If this parameter is empty, all fields as specified in [[fields()]] will be returned. - * @param array $expand the additional fields that the output array should contain. - * Fields not specified in [[extraFields()]] will be ignored. If this parameter is empty, no extra fields - * will be returned. - * @param boolean $recursive whether to recursively return array representation of embedded objects. - * @return array the array representation of the object - */ - public function toArray(array $fields = [], array $expand = [], $recursive = true); + /** + * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. + * + * A field is a named element in the returned array by [[toArray()]]. + * + * This method should return an array of field names or field definitions. + * If the former, the field name will be treated as an object property name whose value will be used + * as the field value. If the latter, the array key should be the field name while the array value should be + * the corresponding field definition which can be either an object property name or a PHP callable + * returning the corresponding field value. The signature of the callable should be: + * + * ```php + * function ($field, $model) { + * // return field value + * } + * ``` + * + * For example, the following code declares four fields: + * + * - `email`: the field name is the same as the property name `email`; + * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their + * values are obtained from the `first_name` and `last_name` properties; + * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` + * and `last_name`. + * + * ```php + * return [ + * 'email', + * 'firstName' => 'first_name', + * 'lastName' => 'last_name', + * 'fullName' => function () { + * return $this->first_name . ' ' . $this->last_name; + * }, + * ]; + * ``` + * + * @return array the list of field names or field definitions. + * @see toArray() + */ + public function fields(); + /** + * Returns the list of additional fields that can be returned by [[toArray()]] in addition to those listed in [[fields()]]. + * + * This method is similar to [[fields()]] except that the list of fields declared + * by this method are not returned by default by [[toArray()]]. Only when a field in the list + * is explicitly requested, will it be included in the result of [[toArray()]]. + * + * @return array the list of expandable field names or field definitions. Please refer + * to [[fields()]] on the format of the return value. + * @see toArray() + * @see fields() + */ + public function extraFields(); + /** + * Converts the object into an array. + * + * @param array $fields the fields that the output array should contain. Fields not specified + * in [[fields()]] will be ignored. If this parameter is empty, all fields as specified in [[fields()]] will be returned. + * @param array $expand the additional fields that the output array should contain. + * Fields not specified in [[extraFields()]] will be ignored. If this parameter is empty, no extra fields + * will be returned. + * @param boolean $recursive whether to recursively return array representation of embedded objects. + * @return array the array representation of the object + */ + public function toArray(array $fields = [], array $expand = [], $recursive = true); } diff --git a/framework/base/ArrayableTrait.php b/framework/base/ArrayableTrait.php index 344521efbd5..b30722b9986 100644 --- a/framework/base/ArrayableTrait.php +++ b/framework/base/ArrayableTrait.php @@ -23,145 +23,146 @@ */ trait ArrayableTrait { - /** - * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. - * - * A field is a named element in the returned array by [[toArray()]]. - * - * This method should return an array of field names or field definitions. - * If the former, the field name will be treated as an object property name whose value will be used - * as the field value. If the latter, the array key should be the field name while the array value should be - * the corresponding field definition which can be either an object property name or a PHP callable - * returning the corresponding field value. The signature of the callable should be: - * - * ```php - * function ($field, $model) { - * // return field value - * } - * ``` - * - * For example, the following code declares four fields: - * - * - `email`: the field name is the same as the property name `email`; - * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their - * values are obtained from the `first_name` and `last_name` properties; - * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` - * and `last_name`. - * - * ```php - * return [ - * 'email', - * 'firstName' => 'first_name', - * 'lastName' => 'last_name', - * 'fullName' => function () { - * return $this->first_name . ' ' . $this->last_name; - * }, - * ]; - * ``` - * - * In this method, you may also want to return different lists of fields based on some context - * information. For example, depending on the privilege of the current application user, - * you may return different sets of visible fields or filter out some fields. - * - * The default implementation of this method returns the public object member variables. - * - * @return array the list of field names or field definitions. - * @see toArray() - */ - public function fields() - { - $fields = array_keys(Yii::getObjectVars($this)); - return array_combine($fields, $fields); - } + /** + * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. + * + * A field is a named element in the returned array by [[toArray()]]. + * + * This method should return an array of field names or field definitions. + * If the former, the field name will be treated as an object property name whose value will be used + * as the field value. If the latter, the array key should be the field name while the array value should be + * the corresponding field definition which can be either an object property name or a PHP callable + * returning the corresponding field value. The signature of the callable should be: + * + * ```php + * function ($field, $model) { + * // return field value + * } + * ``` + * + * For example, the following code declares four fields: + * + * - `email`: the field name is the same as the property name `email`; + * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their + * values are obtained from the `first_name` and `last_name` properties; + * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` + * and `last_name`. + * + * ```php + * return [ + * 'email', + * 'firstName' => 'first_name', + * 'lastName' => 'last_name', + * 'fullName' => function () { + * return $this->first_name . ' ' . $this->last_name; + * }, + * ]; + * ``` + * + * In this method, you may also want to return different lists of fields based on some context + * information. For example, depending on the privilege of the current application user, + * you may return different sets of visible fields or filter out some fields. + * + * The default implementation of this method returns the public object member variables. + * + * @return array the list of field names or field definitions. + * @see toArray() + */ + public function fields() + { + $fields = array_keys(Yii::getObjectVars($this)); - /** - * Returns the list of fields that can be expanded further and returned by [[toArray()]]. - * - * This method is similar to [[fields()]] except that the list of fields returned - * by this method are not returned by default by [[toArray()]]. Only when field names - * to be expanded are explicitly specified when calling [[toArray()]], will their values - * be exported. - * - * The default implementation returns an empty array. - * - * You may override this method to return a list of expandable fields based on some context information - * (e.g. the current application user). - * - * @return array the list of expandable field names or field definitions. Please refer - * to [[fields()]] on the format of the return value. - * @see toArray() - * @see fields() - */ - public function extraFields() - { - return []; - } + return array_combine($fields, $fields); + } - /** - * Converts the model into an array. - * - * This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]]. - * It will then turn the model into an array with these fields. If `$recursive` is true, - * any embedded objects will also be converted into arrays. - * - * If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element - * which refers to a list of links as specified by the interface. - * - * @param array $fields the fields being requested. If empty, all fields as specified by [[fields()]] will be returned. - * @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]] - * will be considered. - * @param boolean $recursive whether to recursively return array representation of embedded objects. - * @return array the array representation of the object - */ - public function toArray(array $fields = [], array $expand = [], $recursive = true) - { - $data = []; - foreach ($this->resolveFields($fields, $expand) as $field => $definition) { - $data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $field, $this); - } + /** + * Returns the list of fields that can be expanded further and returned by [[toArray()]]. + * + * This method is similar to [[fields()]] except that the list of fields returned + * by this method are not returned by default by [[toArray()]]. Only when field names + * to be expanded are explicitly specified when calling [[toArray()]], will their values + * be exported. + * + * The default implementation returns an empty array. + * + * You may override this method to return a list of expandable fields based on some context information + * (e.g. the current application user). + * + * @return array the list of expandable field names or field definitions. Please refer + * to [[fields()]] on the format of the return value. + * @see toArray() + * @see fields() + */ + public function extraFields() + { + return []; + } - if ($this instanceof Linkable) { - $data['_links'] = Link::serialize($this->getLinks()); - } + /** + * Converts the model into an array. + * + * This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]]. + * It will then turn the model into an array with these fields. If `$recursive` is true, + * any embedded objects will also be converted into arrays. + * + * If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element + * which refers to a list of links as specified by the interface. + * + * @param array $fields the fields being requested. If empty, all fields as specified by [[fields()]] will be returned. + * @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]] + * will be considered. + * @param boolean $recursive whether to recursively return array representation of embedded objects. + * @return array the array representation of the object + */ + public function toArray(array $fields = [], array $expand = [], $recursive = true) + { + $data = []; + foreach ($this->resolveFields($fields, $expand) as $field => $definition) { + $data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $field, $this); + } - return $recursive ? ArrayHelper::toArray($data) : $data; - } + if ($this instanceof Linkable) { + $data['_links'] = Link::serialize($this->getLinks()); + } - /** - * Determines which fields can be returned by [[toArray()]]. - * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]] - * to determine which fields can be returned. - * @param array $fields the fields being requested for exporting - * @param array $expand the additional fields being requested for exporting - * @return array the list of fields to be exported. The array keys are the field names, and the array values - * are the corresponding object property names or PHP callables returning the field values. - */ - protected function resolveFields(array $fields, array $expand) - { - $result = []; + return $recursive ? ArrayHelper::toArray($data) : $data; + } - foreach ($this->fields() as $field => $definition) { - if (is_integer($field)) { - $field = $definition; - } - if (empty($fields) || in_array($field, $fields, true)) { - $result[$field] = $definition; - } - } + /** + * Determines which fields can be returned by [[toArray()]]. + * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]] + * to determine which fields can be returned. + * @param array $fields the fields being requested for exporting + * @param array $expand the additional fields being requested for exporting + * @return array the list of fields to be exported. The array keys are the field names, and the array values + * are the corresponding object property names or PHP callables returning the field values. + */ + protected function resolveFields(array $fields, array $expand) + { + $result = []; - if (empty($expand)) { - return $result; - } + foreach ($this->fields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (empty($fields) || in_array($field, $fields, true)) { + $result[$field] = $definition; + } + } - foreach ($this->extraFields() as $field => $definition) { - if (is_integer($field)) { - $field = $definition; - } - if (in_array($field, $expand, true)) { - $result[$field] = $definition; - } - } + if (empty($expand)) { + return $result; + } - return $result; - } + foreach ($this->extraFields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (in_array($field, $expand, true)) { + $result[$field] = $definition; + } + } + + return $result; + } } diff --git a/framework/base/Behavior.php b/framework/base/Behavior.php index 0b1786d7b4d..67911437bd0 100644 --- a/framework/base/Behavior.php +++ b/framework/base/Behavior.php @@ -20,72 +20,72 @@ */ class Behavior extends \yii\base\Object { - /** - * @var Component the owner of this behavior - */ - public $owner; + /** + * @var Component the owner of this behavior + */ + public $owner; - /** - * Declares event handlers for the [[owner]]'s events. - * - * Child classes may override this method to declare what PHP callbacks should - * be attached to the events of the [[owner]] component. - * - * The callbacks will be attached to the [[owner]]'s events when the behavior is - * attached to the owner; and they will be detached from the events when - * the behavior is detached from the component. - * - * The callbacks can be any of the followings: - * - * - method in this behavior: `'handleClick'`, equivalent to `[$this, 'handleClick']` - * - object method: `[$object, 'handleClick']` - * - static method: `['Page', 'handleClick']` - * - anonymous function: `function($event) { ... }` - * - * The following is an example: - * - * ~~~ - * [ - * Model::EVENT_BEFORE_VALIDATE => 'myBeforeValidate', - * Model::EVENT_AFTER_VALIDATE => 'myAfterValidate', - * ] - * ~~~ - * - * @return array events (array keys) and the corresponding event handler methods (array values). - */ - public function events() - { - return []; - } + /** + * Declares event handlers for the [[owner]]'s events. + * + * Child classes may override this method to declare what PHP callbacks should + * be attached to the events of the [[owner]] component. + * + * The callbacks will be attached to the [[owner]]'s events when the behavior is + * attached to the owner; and they will be detached from the events when + * the behavior is detached from the component. + * + * The callbacks can be any of the followings: + * + * - method in this behavior: `'handleClick'`, equivalent to `[$this, 'handleClick']` + * - object method: `[$object, 'handleClick']` + * - static method: `['Page', 'handleClick']` + * - anonymous function: `function ($event) { ... }` + * + * The following is an example: + * + * ~~~ + * [ + * Model::EVENT_BEFORE_VALIDATE => 'myBeforeValidate', + * Model::EVENT_AFTER_VALIDATE => 'myAfterValidate', + * ] + * ~~~ + * + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return []; + } - /** - * Attaches the behavior object to the component. - * The default implementation will set the [[owner]] property - * and attach event handlers as declared in [[events]]. - * Make sure you call the parent implementation if you override this method. - * @param Component $owner the component that this behavior is to be attached to. - */ - public function attach($owner) - { - $this->owner = $owner; - foreach ($this->events() as $event => $handler) { - $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); - } - } + /** + * Attaches the behavior object to the component. + * The default implementation will set the [[owner]] property + * and attach event handlers as declared in [[events]]. + * Make sure you call the parent implementation if you override this method. + * @param Component $owner the component that this behavior is to be attached to. + */ + public function attach($owner) + { + $this->owner = $owner; + foreach ($this->events() as $event => $handler) { + $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); + } + } - /** - * Detaches the behavior object from the component. - * The default implementation will unset the [[owner]] property - * and detach event handlers declared in [[events]]. - * Make sure you call the parent implementation if you override this method. - */ - public function detach() - { - if ($this->owner) { - foreach ($this->events() as $event => $handler) { - $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); - } - $this->owner = null; - } - } + /** + * Detaches the behavior object from the component. + * The default implementation will unset the [[owner]] property + * and detach event handlers declared in [[events]]. + * Make sure you call the parent implementation if you override this method. + */ + public function detach() + { + if ($this->owner) { + foreach ($this->events() as $event => $handler) { + $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); + } + $this->owner = null; + } + } } diff --git a/framework/base/Component.php b/framework/base/Component.php index 596eecaaf81..ad629a41ddc 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -28,7 +28,7 @@ * To attach an event handler to an event, call [[on()]]: * * ~~~ - * $post->on('update', function($event) { + * $post->on('update', function ($event) { * // send email notification * }); * ~~~ @@ -36,7 +36,7 @@ * In the above, an anonymous function is attached to the "update" event of the post. You may attach * the following types of event handlers: * - * - anonymous function: `function($event) { ... }` + * - anonymous function: `function ($event) { ... }` * - object method: `[$object, 'handleAdd']` * - static class method: `['Page', 'handleAdd']` * - global function: `'handleAdd'` @@ -54,7 +54,7 @@ * * ~~~ * [ - * 'on add' => function($event) { ... } + * 'on add' => function ($event) { ... } * ] * ~~~ * @@ -64,7 +64,7 @@ * and then access it when the handler is invoked. You may do so by * * ~~~ - * $post->on('update', function($event) { + * $post->on('update', function ($event) { * // the data can be accessed via $event->data * }, $data); * ~~~ @@ -97,559 +97,577 @@ */ class Component extends Object { - /** - * @var array the attached event handlers (event name => handlers) - */ - private $_events = []; - /** - * @var Behavior[] the attached behaviors (behavior name => behavior) - */ - private $_behaviors; - - /** - * Returns the value of a component property. - * This method will check in the following order and act accordingly: - * - * - a property defined by a getter: return the getter result - * - a property of a behavior: return the behavior property value - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `$value = $component->property;`. - * @param string $name the property name - * @return mixed the property value or the value of a behavior's property - * @throws UnknownPropertyException if the property is not defined - * @throws InvalidCallException if the property is write-only. - * @see __set() - */ - public function __get($name) - { - $getter = 'get' . $name; - if (method_exists($this, $getter)) { - // read property, e.g. getName() - return $this->$getter(); - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canGetProperty($name)) { - return $behavior->$name; - } - } - } - if (method_exists($this, 'set' . $name)) { - throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); - } - } - - /** - * Sets the value of a component property. - * This method will check in the following order and act accordingly: - * - * - a property defined by a setter: set the property value - * - an event in the format of "on xyz": attach the handler to the event "xyz" - * - a behavior in the format of "as xyz": attach the behavior named as "xyz" - * - a property of a behavior: set the behavior property value - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `$component->property = $value;`. - * @param string $name the property name or the event name - * @param mixed $value the property value - * @throws UnknownPropertyException if the property is not defined - * @throws InvalidCallException if the property is read-only. - * @see __get() - */ - public function __set($name, $value) - { - $setter = 'set' . $name; - if (method_exists($this, $setter)) { - // set property - $this->$setter($value); - return; - } elseif (strncmp($name, 'on ', 3) === 0) { - // on event: attach event handler - $this->on(trim(substr($name, 3)), $value); - return; - } elseif (strncmp($name, 'as ', 3) === 0) { - // as behavior: attach behavior - $name = trim(substr($name, 3)); - $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); - return; - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canSetProperty($name)) { - $behavior->$name = $value; - return; - } - } - } - if (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); - } - } - - /** - * Checks if a property value is null. - * This method will check in the following order and act accordingly: - * - * - a property defined by a setter: return whether the property value is null - * - a property of a behavior: return whether the property value is null - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `isset($component->property)`. - * @param string $name the property name or the event name - * @return boolean whether the named property is null - */ - public function __isset($name) - { - $getter = 'get' . $name; - if (method_exists($this, $getter)) { - return $this->$getter() !== null; - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canGetProperty($name)) { - return $behavior->$name !== null; - } - } - } - return false; - } - - /** - * Sets a component property to be null. - * This method will check in the following order and act accordingly: - * - * - a property defined by a setter: set the property value to be null - * - a property of a behavior: set the property value to be null - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `unset($component->property)`. - * @param string $name the property name - * @throws InvalidCallException if the property is read only. - */ - public function __unset($name) - { - $setter = 'set' . $name; - if (method_exists($this, $setter)) { - $this->$setter(null); - return; - } else { - // behavior property - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canSetProperty($name)) { - $behavior->$name = null; - return; - } - } - } - throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name); - } - - /** - * Calls the named method which is not a class method. - * - * This method will check if any attached behavior has - * the named method and will execute it if available. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when an unknown method is being invoked. - * @param string $name the method name - * @param array $params method parameters - * @return mixed the method return value - * @throws UnknownMethodException when calling unknown method - */ - public function __call($name, $params) - { - $this->ensureBehaviors(); - foreach ($this->_behaviors as $object) { - if ($object->hasMethod($name)) { - return call_user_func_array([$object, $name], $params); - } - } - - throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()"); - } - - /** - * This method is called after the object is created by cloning an existing one. - * It removes all behaviors because they are attached to the old object. - */ - public function __clone() - { - $this->_events = []; - $this->_behaviors = null; - } - - /** - * Returns a value indicating whether a property is defined for this component. - * A property is defined if: - * - * - the class has a getter or setter method associated with the specified name - * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVars` is true); - * - an attached behavior has a property of the given name (when `$checkBehaviors` is true). - * - * @param string $name the property name - * @param boolean $checkVars whether to treat member variables as properties - * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component - * @return boolean whether the property is defined - * @see canGetProperty() - * @see canSetProperty() - */ - public function hasProperty($name, $checkVars = true, $checkBehaviors = true) - { - return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors); - } - - /** - * Returns a value indicating whether a property can be read. - * A property can be read if: - * - * - the class has a getter method associated with the specified name - * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVars` is true); - * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true). - * - * @param string $name the property name - * @param boolean $checkVars whether to treat member variables as properties - * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component - * @return boolean whether the property can be read - * @see canSetProperty() - */ - public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) - { - if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) { - return true; - } elseif ($checkBehaviors) { - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canGetProperty($name, $checkVars)) { - return true; - } - } - } - return false; - } - - /** - * Returns a value indicating whether a property can be set. - * A property can be written if: - * - * - the class has a setter method associated with the specified name - * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVars` is true); - * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true). - * - * @param string $name the property name - * @param boolean $checkVars whether to treat member variables as properties - * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component - * @return boolean whether the property can be written - * @see canGetProperty() - */ - public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) - { - if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) { - return true; - } elseif ($checkBehaviors) { - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->canSetProperty($name, $checkVars)) { - return true; - } - } - } - return false; - } - - /** - * Returns a value indicating whether a method is defined. - * A method is defined if: - * - * - the class has a method with the specified name - * - an attached behavior has a method with the given name (when `$checkBehaviors` is true). - * - * @param string $name the property name - * @param boolean $checkBehaviors whether to treat behaviors' methods as methods of this component - * @return boolean whether the property is defined - */ - public function hasMethod($name, $checkBehaviors = true) - { - if (method_exists($this, $name)) { - return true; - } elseif ($checkBehaviors) { - $this->ensureBehaviors(); - foreach ($this->_behaviors as $behavior) { - if ($behavior->hasMethod($name)) { - return true; - } - } - } - return false; - } - - /** - * Returns a list of behaviors that this component should behave as. - * - * Child classes may override this method to specify the behaviors they want to behave as. - * - * The return value of this method should be an array of behavior objects or configurations - * indexed by behavior names. A behavior configuration can be either a string specifying - * the behavior class or an array of the following structure: - * - * ~~~ - * 'behaviorName' => [ - * 'class' => 'BehaviorClass', - * 'property1' => 'value1', - * 'property2' => 'value2', - * ] - * ~~~ - * - * Note that a behavior class must extend from [[Behavior]]. Behavior names can be strings - * or integers. If the former, they uniquely identify the behaviors. If the latter, the corresponding - * behaviors are anonymous and their properties and methods will NOT be made available via the component - * (however, the behaviors can still respond to the component's events). - * - * Behaviors declared in this method will be attached to the component automatically (on demand). - * - * @return array the behavior configurations. - */ - public function behaviors() - { - return []; - } - - /** - * Returns a value indicating whether there is any handler attached to the named event. - * @param string $name the event name - * @return boolean whether there is any handler attached to the event. - */ - public function hasEventHandlers($name) - { - $this->ensureBehaviors(); - return !empty($this->_events[$name]) || Event::hasHandlers($this, $name); - } - - /** - * Attaches an event handler to an event. - * - * The event handler must be a valid PHP callback. The followings are - * some examples: - * - * ~~~ - * function ($event) { ... } // anonymous function - * [$object, 'handleClick'] // $object->handleClick() - * ['Page', 'handleClick'] // Page::handleClick() - * 'handleClick' // global function handleClick() - * ~~~ - * - * The event handler must be defined with the following signature, - * - * ~~~ - * function ($event) - * ~~~ - * - * where `$event` is an [[Event]] object which includes parameters associated with the event. - * - * @param string $name the event name - * @param callable $handler the event handler - * @param mixed $data the data to be passed to the event handler when the event is triggered. - * When the event handler is invoked, this data can be accessed via [[Event::data]]. - * @see off() - */ - public function on($name, $handler, $data = null) - { - $this->ensureBehaviors(); - $this->_events[$name][] = [$handler, $data]; - } - - /** - * Detaches an existing event handler from this component. - * This method is the opposite of [[on()]]. - * @param string $name event name - * @param callable $handler the event handler to be removed. - * If it is null, all handlers attached to the named event will be removed. - * @return boolean if a handler is found and detached - * @see on() - */ - public function off($name, $handler = null) - { - $this->ensureBehaviors(); - if (empty($this->_events[$name])) { - return false; - } - if ($handler === null) { - unset($this->_events[$name]); - return true; - } else { - $removed = false; - foreach ($this->_events[$name] as $i => $event) { - if ($event[0] === $handler) { - unset($this->_events[$name][$i]); - $removed = true; - } - } - if ($removed) { - $this->_events[$name] = array_values($this->_events[$name]); - } - return $removed; - } - } - - /** - * Triggers an event. - * This method represents the happening of an event. It invokes - * all attached handlers for the event including class-level handlers. - * @param string $name the event name - * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. - */ - public function trigger($name, Event $event = null) - { - $this->ensureBehaviors(); - if (!empty($this->_events[$name])) { - if ($event === null) { - $event = new Event; - } - if ($event->sender === null) { - $event->sender = $this; - } - $event->handled = false; - $event->name = $name; - foreach ($this->_events[$name] as $handler) { - $event->data = $handler[1]; - call_user_func($handler[0], $event); - // stop further handling if the event is handled - if ($event->handled) { - return; - } - } - } - // invoke class-level attached handlers - Event::trigger($this, $name, $event); - } - - /** - * Returns the named behavior object. - * @param string $name the behavior name - * @return Behavior the behavior object, or null if the behavior does not exist - */ - public function getBehavior($name) - { - $this->ensureBehaviors(); - return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null; - } - - /** - * Returns all behaviors attached to this component. - * @return Behavior[] list of behaviors attached to this component - */ - public function getBehaviors() - { - $this->ensureBehaviors(); - return $this->_behaviors; - } - - /** - * Attaches a behavior to this component. - * This method will create the behavior object based on the given - * configuration. After that, the behavior object will be attached to - * this component by calling the [[Behavior::attach()]] method. - * @param string $name the name of the behavior. - * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: - * - * - a [[Behavior]] object - * - a string specifying the behavior class - * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object. - * - * @return Behavior the behavior object - * @see detachBehavior() - */ - public function attachBehavior($name, $behavior) - { - $this->ensureBehaviors(); - return $this->attachBehaviorInternal($name, $behavior); - } - - /** - * Attaches a list of behaviors to the component. - * Each behavior is indexed by its name and should be a [[Behavior]] object, - * a string specifying the behavior class, or an configuration array for creating the behavior. - * @param array $behaviors list of behaviors to be attached to the component - * @see attachBehavior() - */ - public function attachBehaviors($behaviors) - { - $this->ensureBehaviors(); - foreach ($behaviors as $name => $behavior) { - $this->attachBehaviorInternal($name, $behavior); - } - } - - /** - * Detaches a behavior from the component. - * The behavior's [[Behavior::detach()]] method will be invoked. - * @param string $name the behavior's name. - * @return Behavior the detached behavior. Null if the behavior does not exist. - */ - public function detachBehavior($name) - { - $this->ensureBehaviors(); - if (isset($this->_behaviors[$name])) { - $behavior = $this->_behaviors[$name]; - unset($this->_behaviors[$name]); - $behavior->detach(); - return $behavior; - } else { - return null; - } - } - - /** - * Detaches all behaviors from the component. - */ - public function detachBehaviors() - { - $this->ensureBehaviors(); - foreach ($this->_behaviors as $name => $behavior) { - $this->detachBehavior($name); - } - } - - /** - * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component. - */ - public function ensureBehaviors() - { - if ($this->_behaviors === null) { - $this->_behaviors = []; - foreach ($this->behaviors() as $name => $behavior) { - $this->attachBehaviorInternal($name, $behavior); - } - } - } - - /** - * Attaches a behavior to this component. - * @param string $name the name of the behavior. - * @param string|array|Behavior $behavior the behavior to be attached - * @return Behavior the attached behavior. - */ - private function attachBehaviorInternal($name, $behavior) - { - if (!($behavior instanceof Behavior)) { - $behavior = Yii::createObject($behavior); - } - if (isset($this->_behaviors[$name])) { - $this->_behaviors[$name]->detach(); - } - $behavior->attach($this); - return $this->_behaviors[$name] = $behavior; - } + /** + * @var array the attached event handlers (event name => handlers) + */ + private $_events = []; + /** + * @var Behavior[] the attached behaviors (behavior name => behavior) + */ + private $_behaviors; + + /** + * Returns the value of a component property. + * This method will check in the following order and act accordingly: + * + * - a property defined by a getter: return the getter result + * - a property of a behavior: return the behavior property value + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `$value = $component->property;`. + * @param string $name the property name + * @return mixed the property value or the value of a behavior's property + * @throws UnknownPropertyException if the property is not defined + * @throws InvalidCallException if the property is write-only. + * @see __set() + */ + public function __get($name) + { + $getter = 'get' . $name; + if (method_exists($this, $getter)) { + // read property, e.g. getName() + return $this->$getter(); + } else { + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canGetProperty($name)) { + return $behavior->$name; + } + } + } + if (method_exists($this, 'set' . $name)) { + throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); + } else { + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); + } + } + + /** + * Sets the value of a component property. + * This method will check in the following order and act accordingly: + * + * - a property defined by a setter: set the property value + * - an event in the format of "on xyz": attach the handler to the event "xyz" + * - a behavior in the format of "as xyz": attach the behavior named as "xyz" + * - a property of a behavior: set the behavior property value + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `$component->property = $value;`. + * @param string $name the property name or the event name + * @param mixed $value the property value + * @throws UnknownPropertyException if the property is not defined + * @throws InvalidCallException if the property is read-only. + * @see __get() + */ + public function __set($name, $value) + { + $setter = 'set' . $name; + if (method_exists($this, $setter)) { + // set property + $this->$setter($value); + + return; + } elseif (strncmp($name, 'on ', 3) === 0) { + // on event: attach event handler + $this->on(trim(substr($name, 3)), $value); + + return; + } elseif (strncmp($name, 'as ', 3) === 0) { + // as behavior: attach behavior + $name = trim(substr($name, 3)); + $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); + + return; + } else { + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canSetProperty($name)) { + $behavior->$name = $value; + + return; + } + } + } + if (method_exists($this, 'get' . $name)) { + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); + } else { + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); + } + } + + /** + * Checks if a property value is null. + * This method will check in the following order and act accordingly: + * + * - a property defined by a setter: return whether the property value is null + * - a property of a behavior: return whether the property value is null + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `isset($component->property)`. + * @param string $name the property name or the event name + * @return boolean whether the named property is null + */ + public function __isset($name) + { + $getter = 'get' . $name; + if (method_exists($this, $getter)) { + return $this->$getter() !== null; + } else { + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canGetProperty($name)) { + return $behavior->$name !== null; + } + } + } + + return false; + } + + /** + * Sets a component property to be null. + * This method will check in the following order and act accordingly: + * + * - a property defined by a setter: set the property value to be null + * - a property of a behavior: set the property value to be null + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `unset($component->property)`. + * @param string $name the property name + * @throws InvalidCallException if the property is read only. + */ + public function __unset($name) + { + $setter = 'set' . $name; + if (method_exists($this, $setter)) { + $this->$setter(null); + + return; + } else { + // behavior property + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canSetProperty($name)) { + $behavior->$name = null; + + return; + } + } + } + throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name); + } + + /** + * Calls the named method which is not a class method. + * + * This method will check if any attached behavior has + * the named method and will execute it if available. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when an unknown method is being invoked. + * @param string $name the method name + * @param array $params method parameters + * @return mixed the method return value + * @throws UnknownMethodException when calling unknown method + */ + public function __call($name, $params) + { + $this->ensureBehaviors(); + foreach ($this->_behaviors as $object) { + if ($object->hasMethod($name)) { + return call_user_func_array([$object, $name], $params); + } + } + + throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()"); + } + + /** + * This method is called after the object is created by cloning an existing one. + * It removes all behaviors because they are attached to the old object. + */ + public function __clone() + { + $this->_events = []; + $this->_behaviors = null; + } + + /** + * Returns a value indicating whether a property is defined for this component. + * A property is defined if: + * + * - the class has a getter or setter method associated with the specified name + * (in this case, property name is case-insensitive); + * - the class has a member variable with the specified name (when `$checkVars` is true); + * - an attached behavior has a property of the given name (when `$checkBehaviors` is true). + * + * @param string $name the property name + * @param boolean $checkVars whether to treat member variables as properties + * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component + * @return boolean whether the property is defined + * @see canGetProperty() + * @see canSetProperty() + */ + public function hasProperty($name, $checkVars = true, $checkBehaviors = true) + { + return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors); + } + + /** + * Returns a value indicating whether a property can be read. + * A property can be read if: + * + * - the class has a getter method associated with the specified name + * (in this case, property name is case-insensitive); + * - the class has a member variable with the specified name (when `$checkVars` is true); + * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true). + * + * @param string $name the property name + * @param boolean $checkVars whether to treat member variables as properties + * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component + * @return boolean whether the property can be read + * @see canSetProperty() + */ + public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) + { + if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) { + return true; + } elseif ($checkBehaviors) { + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canGetProperty($name, $checkVars)) { + return true; + } + } + } + + return false; + } + + /** + * Returns a value indicating whether a property can be set. + * A property can be written if: + * + * - the class has a setter method associated with the specified name + * (in this case, property name is case-insensitive); + * - the class has a member variable with the specified name (when `$checkVars` is true); + * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true). + * + * @param string $name the property name + * @param boolean $checkVars whether to treat member variables as properties + * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component + * @return boolean whether the property can be written + * @see canGetProperty() + */ + public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) + { + if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) { + return true; + } elseif ($checkBehaviors) { + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->canSetProperty($name, $checkVars)) { + return true; + } + } + } + + return false; + } + + /** + * Returns a value indicating whether a method is defined. + * A method is defined if: + * + * - the class has a method with the specified name + * - an attached behavior has a method with the given name (when `$checkBehaviors` is true). + * + * @param string $name the property name + * @param boolean $checkBehaviors whether to treat behaviors' methods as methods of this component + * @return boolean whether the property is defined + */ + public function hasMethod($name, $checkBehaviors = true) + { + if (method_exists($this, $name)) { + return true; + } elseif ($checkBehaviors) { + $this->ensureBehaviors(); + foreach ($this->_behaviors as $behavior) { + if ($behavior->hasMethod($name)) { + return true; + } + } + } + + return false; + } + + /** + * Returns a list of behaviors that this component should behave as. + * + * Child classes may override this method to specify the behaviors they want to behave as. + * + * The return value of this method should be an array of behavior objects or configurations + * indexed by behavior names. A behavior configuration can be either a string specifying + * the behavior class or an array of the following structure: + * + * ~~~ + * 'behaviorName' => [ + * 'class' => 'BehaviorClass', + * 'property1' => 'value1', + * 'property2' => 'value2', + * ] + * ~~~ + * + * Note that a behavior class must extend from [[Behavior]]. Behavior names can be strings + * or integers. If the former, they uniquely identify the behaviors. If the latter, the corresponding + * behaviors are anonymous and their properties and methods will NOT be made available via the component + * (however, the behaviors can still respond to the component's events). + * + * Behaviors declared in this method will be attached to the component automatically (on demand). + * + * @return array the behavior configurations. + */ + public function behaviors() + { + return []; + } + + /** + * Returns a value indicating whether there is any handler attached to the named event. + * @param string $name the event name + * @return boolean whether there is any handler attached to the event. + */ + public function hasEventHandlers($name) + { + $this->ensureBehaviors(); + + return !empty($this->_events[$name]) || Event::hasHandlers($this, $name); + } + + /** + * Attaches an event handler to an event. + * + * The event handler must be a valid PHP callback. The followings are + * some examples: + * + * ~~~ + * function ($event) { ... } // anonymous function + * [$object, 'handleClick'] // $object->handleClick() + * ['Page', 'handleClick'] // Page::handleClick() + * 'handleClick' // global function handleClick() + * ~~~ + * + * The event handler must be defined with the following signature, + * + * ~~~ + * function ($event) + * ~~~ + * + * where `$event` is an [[Event]] object which includes parameters associated with the event. + * + * @param string $name the event name + * @param callable $handler the event handler + * @param mixed $data the data to be passed to the event handler when the event is triggered. + * When the event handler is invoked, this data can be accessed via [[Event::data]]. + * @see off() + */ + public function on($name, $handler, $data = null) + { + $this->ensureBehaviors(); + $this->_events[$name][] = [$handler, $data]; + } + + /** + * Detaches an existing event handler from this component. + * This method is the opposite of [[on()]]. + * @param string $name event name + * @param callable $handler the event handler to be removed. + * If it is null, all handlers attached to the named event will be removed. + * @return boolean if a handler is found and detached + * @see on() + */ + public function off($name, $handler = null) + { + $this->ensureBehaviors(); + if (empty($this->_events[$name])) { + return false; + } + if ($handler === null) { + unset($this->_events[$name]); + + return true; + } else { + $removed = false; + foreach ($this->_events[$name] as $i => $event) { + if ($event[0] === $handler) { + unset($this->_events[$name][$i]); + $removed = true; + } + } + if ($removed) { + $this->_events[$name] = array_values($this->_events[$name]); + } + + return $removed; + } + } + + /** + * Triggers an event. + * This method represents the happening of an event. It invokes + * all attached handlers for the event including class-level handlers. + * @param string $name the event name + * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. + */ + public function trigger($name, Event $event = null) + { + $this->ensureBehaviors(); + if (!empty($this->_events[$name])) { + if ($event === null) { + $event = new Event; + } + if ($event->sender === null) { + $event->sender = $this; + } + $event->handled = false; + $event->name = $name; + foreach ($this->_events[$name] as $handler) { + $event->data = $handler[1]; + call_user_func($handler[0], $event); + // stop further handling if the event is handled + if ($event->handled) { + return; + } + } + } + // invoke class-level attached handlers + Event::trigger($this, $name, $event); + } + + /** + * Returns the named behavior object. + * @param string $name the behavior name + * @return Behavior the behavior object, or null if the behavior does not exist + */ + public function getBehavior($name) + { + $this->ensureBehaviors(); + + return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null; + } + + /** + * Returns all behaviors attached to this component. + * @return Behavior[] list of behaviors attached to this component + */ + public function getBehaviors() + { + $this->ensureBehaviors(); + + return $this->_behaviors; + } + + /** + * Attaches a behavior to this component. + * This method will create the behavior object based on the given + * configuration. After that, the behavior object will be attached to + * this component by calling the [[Behavior::attach()]] method. + * @param string $name the name of the behavior. + * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: + * + * - a [[Behavior]] object + * - a string specifying the behavior class + * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object. + * + * @return Behavior the behavior object + * @see detachBehavior() + */ + public function attachBehavior($name, $behavior) + { + $this->ensureBehaviors(); + + return $this->attachBehaviorInternal($name, $behavior); + } + + /** + * Attaches a list of behaviors to the component. + * Each behavior is indexed by its name and should be a [[Behavior]] object, + * a string specifying the behavior class, or an configuration array for creating the behavior. + * @param array $behaviors list of behaviors to be attached to the component + * @see attachBehavior() + */ + public function attachBehaviors($behaviors) + { + $this->ensureBehaviors(); + foreach ($behaviors as $name => $behavior) { + $this->attachBehaviorInternal($name, $behavior); + } + } + + /** + * Detaches a behavior from the component. + * The behavior's [[Behavior::detach()]] method will be invoked. + * @param string $name the behavior's name. + * @return Behavior the detached behavior. Null if the behavior does not exist. + */ + public function detachBehavior($name) + { + $this->ensureBehaviors(); + if (isset($this->_behaviors[$name])) { + $behavior = $this->_behaviors[$name]; + unset($this->_behaviors[$name]); + $behavior->detach(); + + return $behavior; + } else { + return null; + } + } + + /** + * Detaches all behaviors from the component. + */ + public function detachBehaviors() + { + $this->ensureBehaviors(); + foreach ($this->_behaviors as $name => $behavior) { + $this->detachBehavior($name); + } + } + + /** + * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component. + */ + public function ensureBehaviors() + { + if ($this->_behaviors === null) { + $this->_behaviors = []; + foreach ($this->behaviors() as $name => $behavior) { + $this->attachBehaviorInternal($name, $behavior); + } + } + } + + /** + * Attaches a behavior to this component. + * @param string $name the name of the behavior. + * @param string|array|Behavior $behavior the behavior to be attached + * @return Behavior the attached behavior. + */ + private function attachBehaviorInternal($name, $behavior) + { + if (!($behavior instanceof Behavior)) { + $behavior = Yii::createObject($behavior); + } + if (isset($this->_behaviors[$name])) { + $this->_behaviors[$name]->detach(); + } + $behavior->attach($this); + + return $this->_behaviors[$name] = $behavior; + } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 27d234512bc..1a1d6bb9220 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -25,395 +25,400 @@ */ class Controller extends Component implements ViewContextInterface { - /** - * @event ActionEvent an event raised right before executing a controller action. - * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. - */ - const EVENT_BEFORE_ACTION = 'beforeAction'; - /** - * @event ActionEvent an event raised right after executing a controller action. - */ - const EVENT_AFTER_ACTION = 'afterAction'; - /** - * @var string the ID of this controller. - */ - public $id; - /** - * @var Module $module the module that this controller belongs to. - */ - public $module; - /** - * @var string the ID of the action that is used when the action ID is not specified - * in the request. Defaults to 'index'. - */ - public $defaultAction = 'index'; - /** - * @var string|boolean the name of the layout to be applied to this controller's views. - * This property mainly affects the behavior of [[render()]]. - * Defaults to null, meaning the actual layout value should inherit that from [[module]]'s layout value. - * If false, no layout will be applied. - */ - public $layout; - /** - * @var Action the action that is currently being executed. This property will be set - * by [[run()]] when it is called by [[Application]] to run an action. - */ - public $action; - /** - * @var View the view object that can be used to render views or view files. - */ - private $_view; + /** + * @event ActionEvent an event raised right before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised right after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + /** + * @var string the ID of this controller. + */ + public $id; + /** + * @var Module $module the module that this controller belongs to. + */ + public $module; + /** + * @var string the ID of the action that is used when the action ID is not specified + * in the request. Defaults to 'index'. + */ + public $defaultAction = 'index'; + /** + * @var string|boolean the name of the layout to be applied to this controller's views. + * This property mainly affects the behavior of [[render()]]. + * Defaults to null, meaning the actual layout value should inherit that from [[module]]'s layout value. + * If false, no layout will be applied. + */ + public $layout; + /** + * @var Action the action that is currently being executed. This property will be set + * by [[run()]] when it is called by [[Application]] to run an action. + */ + public $action; + /** + * @var View the view object that can be used to render views or view files. + */ + private $_view; + /** + * @param string $id the ID of this controller. + * @param Module $module the module that this controller belongs to. + * @param array $config name-value pairs that will be used to initialize the object properties. + */ + public function __construct($id, $module, $config = []) + { + $this->id = $id; + $this->module = $module; + parent::__construct($config); + } - /** - * @param string $id the ID of this controller. - * @param Module $module the module that this controller belongs to. - * @param array $config name-value pairs that will be used to initialize the object properties. - */ - public function __construct($id, $module, $config = []) - { - $this->id = $id; - $this->module = $module; - parent::__construct($config); - } + /** + * Declares external actions for the controller. + * This method is meant to be overwritten to declare external actions for the controller. + * It should return an array, with array keys being action IDs, and array values the corresponding + * action class names or action configuration arrays. For example, + * + * ~~~ + * return [ + * 'action1' => 'app\components\Action1', + * 'action2' => [ + * 'class' => 'app\components\Action2', + * 'property1' => 'value1', + * 'property2' => 'value2', + * ], + * ]; + * ~~~ + * + * [[\Yii::createObject()]] will be used later to create the requested action + * using the configuration provided here. + */ + public function actions() + { + return []; + } - /** - * Declares external actions for the controller. - * This method is meant to be overwritten to declare external actions for the controller. - * It should return an array, with array keys being action IDs, and array values the corresponding - * action class names or action configuration arrays. For example, - * - * ~~~ - * return [ - * 'action1' => 'app\components\Action1', - * 'action2' => [ - * 'class' => 'app\components\Action2', - * 'property1' => 'value1', - * 'property2' => 'value2', - * ], - * ]; - * ~~~ - * - * [[\Yii::createObject()]] will be used later to create the requested action - * using the configuration provided here. - */ - public function actions() - { - return []; - } + /** + * Runs an action within this controller with the specified action ID and parameters. + * If the action ID is empty, the method will use [[defaultAction]]. + * @param string $id the ID of the action to be executed. + * @param array $params the parameters (name-value pairs) to be passed to the action. + * @return mixed the result of the action. + * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. + * @see createAction() + */ + public function runAction($id, $params = []) + { + $action = $this->createAction($id); + if ($action !== null) { + Yii::trace("Route to run: " . $action->getUniqueId(), __METHOD__); + if (Yii::$app->requestedAction === null) { + Yii::$app->requestedAction = $action; + } + $oldAction = $this->action; + $this->action = $action; + $result = null; + $event = new ActionEvent($action); + Yii::$app->trigger(Application::EVENT_BEFORE_ACTION, $event); + if ($event->isValid && $this->module->beforeAction($action) && $this->beforeAction($action)) { + $result = $action->runWithParams($params); + $result = $this->afterAction($action, $result); + $result = $this->module->afterAction($action, $result); + $event = new ActionEvent($action); + $event->result = $result; + Yii::$app->trigger(Application::EVENT_AFTER_ACTION, $event); + $result = $event->result; + } + $this->action = $oldAction; - /** - * Runs an action within this controller with the specified action ID and parameters. - * If the action ID is empty, the method will use [[defaultAction]]. - * @param string $id the ID of the action to be executed. - * @param array $params the parameters (name-value pairs) to be passed to the action. - * @return mixed the result of the action. - * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. - * @see createAction() - */ - public function runAction($id, $params = []) - { - $action = $this->createAction($id); - if ($action !== null) { - Yii::trace("Route to run: " . $action->getUniqueId(), __METHOD__); - if (Yii::$app->requestedAction === null) { - Yii::$app->requestedAction = $action; - } - $oldAction = $this->action; - $this->action = $action; - $result = null; - $event = new ActionEvent($action); - Yii::$app->trigger(Application::EVENT_BEFORE_ACTION, $event); - if ($event->isValid && $this->module->beforeAction($action) && $this->beforeAction($action)) { - $result = $action->runWithParams($params); - $result = $this->afterAction($action, $result); - $result = $this->module->afterAction($action, $result); - $event = new ActionEvent($action); - $event->result = $result; - Yii::$app->trigger(Application::EVENT_AFTER_ACTION, $event); - $result = $event->result; - } - $this->action = $oldAction; - return $result; - } else { - throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id); - } - } + return $result; + } else { + throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id); + } + } - /** - * Runs a request specified in terms of a route. - * The route can be either an ID of an action within this controller or a complete route consisting - * of module IDs, controller ID and action ID. If the route starts with a slash '/', the parsing of - * the route will start from the application; otherwise, it will start from the parent module of this controller. - * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. - * @param array $params the parameters to be passed to the action. - * @return mixed the result of the action. - * @see runAction() - */ - public function run($route, $params = []) - { - $pos = strpos($route, '/'); - if ($pos === false) { - return $this->runAction($route, $params); - } elseif ($pos > 0) { - return $this->module->runAction($route, $params); - } else { - return Yii::$app->runAction(ltrim($route, '/'), $params); - } - } + /** + * Runs a request specified in terms of a route. + * The route can be either an ID of an action within this controller or a complete route consisting + * of module IDs, controller ID and action ID. If the route starts with a slash '/', the parsing of + * the route will start from the application; otherwise, it will start from the parent module of this controller. + * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. + * @param array $params the parameters to be passed to the action. + * @return mixed the result of the action. + * @see runAction() + */ + public function run($route, $params = []) + { + $pos = strpos($route, '/'); + if ($pos === false) { + return $this->runAction($route, $params); + } elseif ($pos > 0) { + return $this->module->runAction($route, $params); + } else { + return Yii::$app->runAction(ltrim($route, '/'), $params); + } + } - /** - * Binds the parameters to the action. - * This method is invoked by [[Action]] when it begins to run with the given parameters. - * @param Action $action the action to be bound with parameters. - * @param array $params the parameters to be bound to the action. - * @return array the valid parameters that the action can run with. - */ - public function bindActionParams($action, $params) - { - return []; - } + /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * @param Action $action the action to be bound with parameters. + * @param array $params the parameters to be bound to the action. + * @return array the valid parameters that the action can run with. + */ + public function bindActionParams($action, $params) + { + return []; + } - /** - * Creates an action based on the given action ID. - * The method first checks if the action ID has been declared in [[actions()]]. If so, - * it will use the configuration declared there to create the action object. - * If not, it will look for a controller method whose name is in the format of `actionXyz` - * where `Xyz` stands for the action ID. If found, an [[InlineAction]] representing that - * method will be created and returned. - * @param string $id the action ID. - * @return Action the newly created action instance. Null if the ID doesn't resolve into any action. - */ - public function createAction($id) - { - if ($id === '') { - $id = $this->defaultAction; - } + /** + * Creates an action based on the given action ID. + * The method first checks if the action ID has been declared in [[actions()]]. If so, + * it will use the configuration declared there to create the action object. + * If not, it will look for a controller method whose name is in the format of `actionXyz` + * where `Xyz` stands for the action ID. If found, an [[InlineAction]] representing that + * method will be created and returned. + * @param string $id the action ID. + * @return Action the newly created action instance. Null if the ID doesn't resolve into any action. + */ + public function createAction($id) + { + if ($id === '') { + $id = $this->defaultAction; + } - $actionMap = $this->actions(); - if (isset($actionMap[$id])) { - return Yii::createObject($actionMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) { - $methodName = 'action' . str_replace(' ', '', ucwords(implode(' ', explode('-', $id)))); - if (method_exists($this, $methodName)) { - $method = new \ReflectionMethod($this, $methodName); - if ($method->getName() === $methodName) { - return new InlineAction($id, $this, $methodName); - } - } - } - return null; - } + $actionMap = $this->actions(); + if (isset($actionMap[$id])) { + return Yii::createObject($actionMap[$id], $id, $this); + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) { + $methodName = 'action' . str_replace(' ', '', ucwords(implode(' ', explode('-', $id)))); + if (method_exists($this, $methodName)) { + $method = new \ReflectionMethod($this, $methodName); + if ($method->getName() === $methodName) { + return new InlineAction($id, $this, $methodName); + } + } + } - /** - * This method is invoked right before an action is to be executed (after all possible filters). - * You may override this method to do last-minute preparation for the action. - * If you override this method, please make sure you call the parent implementation first. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $event = new ActionEvent($action); - $this->trigger(self::EVENT_BEFORE_ACTION, $event); - return $event->isValid; - } + return null; + } - /** - * This method is invoked right after an action is executed. - * You may override this method to do some postprocessing for the action. - * If you override this method, please make sure you call the parent implementation first. - * Also make sure you return the action result, whether it is processed or not. - * @param Action $action the action just executed. - * @param mixed $result the action return result. - * @return mixed the processed action result. - */ - public function afterAction($action, $result) - { - $event = new ActionEvent($action); - $event->result = $result; - $this->trigger(self::EVENT_AFTER_ACTION, $event); - return $event->result; - } + /** + * This method is invoked right before an action is to be executed (after all possible filters). + * You may override this method to do last-minute preparation for the action. + * If you override this method, please make sure you call the parent implementation first. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $event = new ActionEvent($action); + $this->trigger(self::EVENT_BEFORE_ACTION, $event); - /** - * @return string the controller ID that is prefixed with the module ID (if any). - */ - public function getUniqueId() - { - return $this->module instanceof Application ? $this->id : $this->module->getUniqueId() . '/' . $this->id; - } + return $event->isValid; + } - /** - * Returns the route of the current request. - * @return string the route (module ID, controller ID and action ID) of the current request. - */ - public function getRoute() - { - return $this->action !== null ? $this->action->getUniqueId() : $this->getUniqueId(); - } + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * If you override this method, please make sure you call the parent implementation first. + * Also make sure you return the action result, whether it is processed or not. + * @param Action $action the action just executed. + * @param mixed $result the action return result. + * @return mixed the processed action result. + */ + public function afterAction($action, $result) + { + $event = new ActionEvent($action); + $event->result = $result; + $this->trigger(self::EVENT_AFTER_ACTION, $event); - /** - * Renders a view and applies layout if available. - * - * The view to be rendered can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]]. - * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. - * - * To determine which layout should be applied, the following two steps are conducted: - * - * 1. In the first step, it determines the layout name and the context module: - * - * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; - * - If [[layout]] is null, search through all ancestor modules of this controller and find the first - * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module - * are used as the layout name and the context module, respectively. If such a module is not found - * or the corresponding layout is not a string, it will return false, meaning no applicable layout. - * - * 2. In the second step, it determines the actual layout file according to the previously found layout name - * and context module. The layout name can be: - * - * - a path alias (e.g. "@app/views/layouts/main"); - * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be - * looked for under the [[Application::layoutPath|layout path]] of the application; - * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::layoutPath|layout path]] of the context module. - * - * If the layout name does not contain a file extension, it will use the default one `.php`. - * - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. - * @param array $params the parameters (name-value pairs) that should be made available in the view. - * These parameters will not be available in the layout. - * @return string the rendering result. - * @throws InvalidParamException if the view file or the layout file does not exist. - */ - public function render($view, $params = []) - { - $output = $this->getView()->render($view, $params, $this); - $layoutFile = $this->findLayoutFile($this->getView()); - if ($layoutFile !== false) { - return $this->getView()->renderFile($layoutFile, ['content' => $output], $this); - } else { - return $output; - } - } + return $event->result; + } - /** - * Renders a view. - * This method differs from [[render()]] in that it does not apply any layout. - * @param string $view the view name. Please refer to [[render()]] on how to specify a view name. - * @param array $params the parameters (name-value pairs) that should be made available in the view. - * @return string the rendering result. - * @throws InvalidParamException if the view file does not exist. - */ - public function renderPartial($view, $params = []) - { - return $this->getView()->render($view, $params, $this); - } + /** + * @return string the controller ID that is prefixed with the module ID (if any). + */ + public function getUniqueId() + { + return $this->module instanceof Application ? $this->id : $this->module->getUniqueId() . '/' . $this->id; + } - /** - * Renders a view file. - * @param string $file the view file to be rendered. This can be either a file path or a path alias. - * @param array $params the parameters (name-value pairs) that should be made available in the view. - * @return string the rendering result. - * @throws InvalidParamException if the view file does not exist. - */ - public function renderFile($file, $params = []) - { - return $this->getView()->renderFile($file, $params, $this); - } + /** + * Returns the route of the current request. + * @return string the route (module ID, controller ID and action ID) of the current request. + */ + public function getRoute() + { + return $this->action !== null ? $this->action->getUniqueId() : $this->getUniqueId(); + } - /** - * Returns the view object that can be used to render views or view files. - * The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use - * this view object to implement the actual view rendering. - * If not set, it will default to the "view" application component. - * @return View the view object that can be used to render views or view files. - */ - public function getView() - { - if ($this->_view === null) { - $this->_view = Yii::$app->getView(); - } - return $this->_view; - } + /** + * Renders a view and applies layout if available. + * + * The view to be rendered can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]]. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * To determine which layout should be applied, the following two steps are conducted: + * + * 1. In the first step, it determines the layout name and the context module: + * + * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; + * - If [[layout]] is null, search through all ancestor modules of this controller and find the first + * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module + * are used as the layout name and the context module, respectively. If such a module is not found + * or the corresponding layout is not a string, it will return false, meaning no applicable layout. + * + * 2. In the second step, it determines the actual layout file according to the previously found layout name + * and context module. The layout name can be: + * + * - a path alias (e.g. "@app/views/layouts/main"); + * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be + * looked for under the [[Application::layoutPath|layout path]] of the application; + * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the + * [[Module::layoutPath|layout path]] of the context module. + * + * If the layout name does not contain a file extension, it will use the default one `.php`. + * + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * These parameters will not be available in the layout. + * @return string the rendering result. + * @throws InvalidParamException if the view file or the layout file does not exist. + */ + public function render($view, $params = []) + { + $output = $this->getView()->render($view, $params, $this); + $layoutFile = $this->findLayoutFile($this->getView()); + if ($layoutFile !== false) { + return $this->getView()->renderFile($layoutFile, ['content' => $output], $this); + } else { + return $output; + } + } - /** - * Sets the view object to be used by this controller. - * @param View $view the view object that can be used to render views or view files. - */ - public function setView($view) - { - $this->_view = $view; - } + /** + * Renders a view. + * This method differs from [[render()]] in that it does not apply any layout. + * @param string $view the view name. Please refer to [[render()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ + public function renderPartial($view, $params = []) + { + return $this->getView()->render($view, $params, $this); + } - /** - * Returns the directory containing view files for this controller. - * The default implementation returns the directory named as controller [[id]] under the [[module]]'s - * [[viewPath]] directory. - * @return string the directory containing the view files for this controller. - */ - public function getViewPath() - { - return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; - } + /** + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ + public function renderFile($file, $params = []) + { + return $this->getView()->renderFile($file, $params, $this); + } - /** - * Finds the view file based on the given view name. - * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] - * on how to specify this parameter. - * @return string the view file path. Note that the file may not exist. - */ - public function findViewFile($view) - { - return $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } + /** + * Returns the view object that can be used to render views or view files. + * The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use + * this view object to implement the actual view rendering. + * If not set, it will default to the "view" application component. + * @return View the view object that can be used to render views or view files. + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = Yii::$app->getView(); + } - /** - * Finds the applicable layout file. - * @param View $view the view object to render the layout file. - * @return string|boolean the layout file path, or false if layout is not needed. - * Please refer to [[render()]] on how to specify this parameter. - * @throws InvalidParamException if an invalid path alias is used to specify the layout. - */ - protected function findLayoutFile($view) - { - $module = $this->module; - if (is_string($this->layout)) { - $layout = $this->layout; - } elseif ($this->layout === null) { - while ($module !== null && $module->layout === null) { - $module = $module->module; - } - if ($module !== null && is_string($module->layout)) { - $layout = $module->layout; - } - } + return $this->_view; + } - if (!isset($layout)) { - return false; - } + /** + * Sets the view object to be used by this controller. + * @param View $view the view object that can be used to render views or view files. + */ + public function setView($view) + { + $this->_view = $view; + } - if (strncmp($layout, '@', 1) === 0) { - $file = Yii::getAlias($layout); - } elseif (strncmp($layout, '/', 1) === 0) { - $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . substr($layout, 1); - } else { - $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $layout; - } + /** + * Returns the directory containing view files for this controller. + * The default implementation returns the directory named as controller [[id]] under the [[module]]'s + * [[viewPath]] directory. + * @return string the directory containing the view files for this controller. + */ + public function getViewPath() + { + return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; + } - if (pathinfo($file, PATHINFO_EXTENSION) !== '') { - return $file; - } - $path = $file . '.' . $view->defaultExtension; - if ($view->defaultExtension !== 'php' && !is_file($path)) { - $path = $file . '.php'; - } - return $path; - } + /** + * Finds the view file based on the given view name. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view) + { + return $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } + + /** + * Finds the applicable layout file. + * @param View $view the view object to render the layout file. + * @return string|boolean the layout file path, or false if layout is not needed. + * Please refer to [[render()]] on how to specify this parameter. + * @throws InvalidParamException if an invalid path alias is used to specify the layout. + */ + protected function findLayoutFile($view) + { + $module = $this->module; + if (is_string($this->layout)) { + $layout = $this->layout; + } elseif ($this->layout === null) { + while ($module !== null && $module->layout === null) { + $module = $module->module; + } + if ($module !== null && is_string($module->layout)) { + $layout = $module->layout; + } + } + + if (!isset($layout)) { + return false; + } + + if (strncmp($layout, '@', 1) === 0) { + $file = Yii::getAlias($layout); + } elseif (strncmp($layout, '/', 1) === 0) { + $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . substr($layout, 1); + } else { + $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $layout; + } + + if (pathinfo($file, PATHINFO_EXTENSION) !== '') { + return $file; + } + $path = $file . '.' . $view->defaultExtension; + if ($view->defaultExtension !== 'php' && !is_file($path)) { + $path = $file . '.php'; + } + + return $path; + } } diff --git a/framework/base/DynamicModel.php b/framework/base/DynamicModel.php index b93648da046..afe94f4c06b 100644 --- a/framework/base/DynamicModel.php +++ b/framework/base/DynamicModel.php @@ -55,143 +55,145 @@ */ class DynamicModel extends Model { - private $_attributes = []; + private $_attributes = []; - /** - * Constructors. - * @param array $attributes the dynamic attributes (name-value pairs, or names) being defined - * @param array $config the configuration array to be applied to this object. - */ - public function __construct(array $attributes = [], $config = []) - { - foreach ($attributes as $name => $value) { - if (is_integer($name)) { - $this->_attributes[$value] = null; - } else { - $this->_attributes[$name] = $value; - } - } - } + /** + * Constructors. + * @param array $attributes the dynamic attributes (name-value pairs, or names) being defined + * @param array $config the configuration array to be applied to this object. + */ + public function __construct(array $attributes = [], $config = []) + { + foreach ($attributes as $name => $value) { + if (is_integer($name)) { + $this->_attributes[$value] = null; + } else { + $this->_attributes[$name] = $value; + } + } + } - /** - * @inheritdoc - */ - public function __get($name) - { - if (array_key_exists($name, $this->_attributes)) { - return $this->_attributes[$name]; - } else { - return parent::__get($name); - } - } + /** + * @inheritdoc + */ + public function __get($name) + { + if (array_key_exists($name, $this->_attributes)) { + return $this->_attributes[$name]; + } else { + return parent::__get($name); + } + } - /** - * @inheritdoc - */ - public function __set($name, $value) - { - if (array_key_exists($name, $this->_attributes)) { - $this->_attributes[$name] = $value; - } else { - parent::__set($name, $value); - } - } + /** + * @inheritdoc + */ + public function __set($name, $value) + { + if (array_key_exists($name, $this->_attributes)) { + $this->_attributes[$name] = $value; + } else { + parent::__set($name, $value); + } + } - /** - * @inheritdoc - */ - public function __isset($name) - { - if (array_key_exists($name, $this->_attributes)) { - return isset($this->_attributes[$name]); - } else { - return parent::__isset($name); - } - } + /** + * @inheritdoc + */ + public function __isset($name) + { + if (array_key_exists($name, $this->_attributes)) { + return isset($this->_attributes[$name]); + } else { + return parent::__isset($name); + } + } - /** - * @inheritdoc - */ - public function __unset($name) - { - if (array_key_exists($name, $this->_attributes)) { - unset($this->_attributes[$name]); - } else { - parent::__unset($name); - } - } + /** + * @inheritdoc + */ + public function __unset($name) + { + if (array_key_exists($name, $this->_attributes)) { + unset($this->_attributes[$name]); + } else { + parent::__unset($name); + } + } - /** - * Defines an attribute. - * @param string $name the attribute name - * @param mixed $value the attribute value - */ - public function defineAttribute($name, $value = null) - { - $this->_attributes[$name] = $value; - } + /** + * Defines an attribute. + * @param string $name the attribute name + * @param mixed $value the attribute value + */ + public function defineAttribute($name, $value = null) + { + $this->_attributes[$name] = $value; + } - /** - * Undefines an attribute. - * @param string $name the attribute name - */ - public function undefineAttribute($name) - { - unset($this->_attributes[$name]); - } + /** + * Undefines an attribute. + * @param string $name the attribute name + */ + public function undefineAttribute($name) + { + unset($this->_attributes[$name]); + } - /** - * Adds a validation rule to this model. - * You can also directly manipulate [[validators]] to add or remove validation rules. - * This method provides a shortcut. - * @param string|array $attributes the attribute(s) to be validated by the rule - * @param mixed $validator the validator for the rule.This can be a built-in validator name, - * a method name of the model class, an anonymous function, or a validator class name. - * @param array $options the options (name-value pairs) to be applied to the validator - * @return static the model itself - */ - public function addRule($attributes, $validator, $options = []) - { - $validators = $this->getValidators(); - $validators->append(Validator::createValidator($validator, $this, (array)$attributes, $options)); - return $this; - } + /** + * Adds a validation rule to this model. + * You can also directly manipulate [[validators]] to add or remove validation rules. + * This method provides a shortcut. + * @param string|array $attributes the attribute(s) to be validated by the rule + * @param mixed $validator the validator for the rule.This can be a built-in validator name, + * a method name of the model class, an anonymous function, or a validator class name. + * @param array $options the options (name-value pairs) to be applied to the validator + * @return static the model itself + */ + public function addRule($attributes, $validator, $options = []) + { + $validators = $this->getValidators(); + $validators->append(Validator::createValidator($validator, $this, (array) $attributes, $options)); - /** - * Validates the given data with the specified validation rules. - * This method will create a DynamicModel instance, populate it with the data to be validated, - * create the specified validation rules, and then validate the data using these rules. - * @param array $data the data (name-value pairs) to be validated - * @param array $rules the validation rules. Please refer to [[Model::rules()]] on the format of this parameter. - * @return static the model instance that contains the data being validated - * @throws InvalidConfigException if a validation rule is not specified correctly. - */ - public static function validateData(array $data, $rules = []) - { - /** @var DynamicModel $model */ - $model = new static($data); - if (!empty($rules)) { - $validators = $model->getValidators(); - foreach ($rules as $rule) { - if ($rule instanceof Validator) { - $validators->append($rule); - } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type - $validator = Validator::createValidator($rule[1], $model, (array) $rule[0], array_slice($rule, 2)); - $validators->append($validator); - } else { - throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); - } - } - $model->validate(); - } - return $model; - } + return $this; + } - /** - * @inheritdoc - */ - public function attributes() - { - return array_keys($this->_attributes); - } + /** + * Validates the given data with the specified validation rules. + * This method will create a DynamicModel instance, populate it with the data to be validated, + * create the specified validation rules, and then validate the data using these rules. + * @param array $data the data (name-value pairs) to be validated + * @param array $rules the validation rules. Please refer to [[Model::rules()]] on the format of this parameter. + * @return static the model instance that contains the data being validated + * @throws InvalidConfigException if a validation rule is not specified correctly. + */ + public static function validateData(array $data, $rules = []) + { + /** @var DynamicModel $model */ + $model = new static($data); + if (!empty($rules)) { + $validators = $model->getValidators(); + foreach ($rules as $rule) { + if ($rule instanceof Validator) { + $validators->append($rule); + } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type + $validator = Validator::createValidator($rule[1], $model, (array) $rule[0], array_slice($rule, 2)); + $validators->append($validator); + } else { + throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); + } + } + $model->validate(); + } + + return $model; + } + + /** + * @inheritdoc + */ + public function attributes() + { + return array_keys($this->_attributes); + } } diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 5f1e9997601..67e825567ff 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -17,80 +17,81 @@ */ class ErrorException extends \ErrorException { - /** - * Constructs the exception. - * @link http://php.net/manual/en/errorexception.construct.php - * @param $message [optional] - * @param $code [optional] - * @param $severity [optional] - * @param $filename [optional] - * @param $lineno [optional] - * @param $previous [optional] - */ - public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, \Exception $previous = null) - { - parent::__construct($message, $code, $previous); - $this->severity = $severity; - $this->file = $filename; - $this->line = $lineno; + /** + * Constructs the exception. + * @link http://php.net/manual/en/errorexception.construct.php + * @param $message [optional] + * @param $code [optional] + * @param $severity [optional] + * @param $filename [optional] + * @param $lineno [optional] + * @param $previous [optional] + */ + public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + $this->severity = $severity; + $this->file = $filename; + $this->line = $lineno; - if (function_exists('xdebug_get_function_stack')) { - $trace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -1); - foreach ($trace as &$frame) { - if (!isset($frame['function'])) { - $frame['function'] = 'unknown'; - } + if (function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -1); + foreach ($trace as &$frame) { + if (!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } - // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 - if (!isset($frame['type']) || $frame['type'] === 'static') { - $frame['type'] = '::'; - } elseif ($frame['type'] === 'dynamic') { - $frame['type'] = '->'; - } + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if (!isset($frame['type']) || $frame['type'] === 'static') { + $frame['type'] = '::'; + } elseif ($frame['type'] === 'dynamic') { + $frame['type'] = '->'; + } - // XDebug has a different key name - if (isset($frame['params']) && !isset($frame['args'])) { - $frame['args'] = $frame['params']; - } - } + // XDebug has a different key name + if (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } - $ref = new \ReflectionProperty('Exception', 'trace'); - $ref->setAccessible(true); - $ref->setValue($this, $trace); - } - } + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($this, $trace); + } + } - /** - * Returns if error is one of fatal type. - * - * @param array $error error got from error_get_last() - * @return boolean if error is one of fatal type - */ - public static function isFatalError($error) - { - return isset($error['type']) && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING]); - } + /** + * Returns if error is one of fatal type. + * + * @param array $error error got from error_get_last() + * @return boolean if error is one of fatal type + */ + public static function isFatalError($error) + { + return isset($error['type']) && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING]); + } - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - $names = [ - E_ERROR => 'PHP Fatal Error', - E_PARSE => 'PHP Parse Error', - E_CORE_ERROR => 'PHP Core Error', - E_COMPILE_ERROR => 'PHP Compile Error', - E_USER_ERROR => 'PHP User Error', - E_WARNING => 'PHP Warning', - E_CORE_WARNING => 'PHP Core Warning', - E_COMPILE_WARNING => 'PHP Compile Warning', - E_USER_WARNING => 'PHP User Warning', - E_STRICT => 'PHP Strict Warning', - E_NOTICE => 'PHP Notice', - E_RECOVERABLE_ERROR => 'PHP Recoverable Error', - E_DEPRECATED => 'PHP Deprecated Warning', - ]; - return isset($names[$this->getCode()]) ? $names[$this->getCode()] : 'Error'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + $names = [ + E_ERROR => 'PHP Fatal Error', + E_PARSE => 'PHP Parse Error', + E_CORE_ERROR => 'PHP Core Error', + E_COMPILE_ERROR => 'PHP Compile Error', + E_USER_ERROR => 'PHP User Error', + E_WARNING => 'PHP Warning', + E_CORE_WARNING => 'PHP Core Warning', + E_COMPILE_WARNING => 'PHP Compile Warning', + E_USER_WARNING => 'PHP User Warning', + E_STRICT => 'PHP Strict Warning', + E_NOTICE => 'PHP Notice', + E_RECOVERABLE_ERROR => 'PHP Recoverable Error', + E_DEPRECATED => 'PHP Deprecated Warning', + ]; + + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : 'Error'; + } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 528c5ab1673..fce20b61d5e 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -25,324 +25,329 @@ */ class ErrorHandler extends Component { - /** - * @var integer maximum number of source code lines to be displayed. Defaults to 25. - */ - public $maxSourceLines = 25; - /** - * @var integer maximum number of trace source code lines to be displayed. Defaults to 10. - */ - public $maxTraceSourceLines = 10; - /** - * @var boolean whether to discard any existing page output before error display. Defaults to true. - */ - public $discardExistingOutput = true; - /** - * @var string the route (e.g. 'site/error') to the controller action that will be used - * to display external errors. Inside the action, it can retrieve the error information - * by Yii::$app->exception. This property defaults to null, meaning ErrorHandler - * will handle the error display. - */ - public $errorAction; - /** - * @var string the path of the view file for rendering exceptions without call stack information. - */ - public $errorView = '@yii/views/errorHandler/error.php'; - /** - * @var string the path of the view file for rendering exceptions. - */ - public $exceptionView = '@yii/views/errorHandler/exception.php'; - /** - * @var string the path of the view file for rendering exceptions and errors call stack element. - */ - public $callStackItemView = '@yii/views/errorHandler/callStackItem.php'; - /** - * @var string the path of the view file for rendering previous exceptions. - */ - public $previousExceptionView = '@yii/views/errorHandler/previousException.php'; - /** - * @var \Exception the exception that is being handled currently. - */ - public $exception; + /** + * @var integer maximum number of source code lines to be displayed. Defaults to 25. + */ + public $maxSourceLines = 25; + /** + * @var integer maximum number of trace source code lines to be displayed. Defaults to 10. + */ + public $maxTraceSourceLines = 10; + /** + * @var boolean whether to discard any existing page output before error display. Defaults to true. + */ + public $discardExistingOutput = true; + /** + * @var string the route (e.g. 'site/error') to the controller action that will be used + * to display external errors. Inside the action, it can retrieve the error information + * by Yii::$app->exception. This property defaults to null, meaning ErrorHandler + * will handle the error display. + */ + public $errorAction; + /** + * @var string the path of the view file for rendering exceptions without call stack information. + */ + public $errorView = '@yii/views/errorHandler/error.php'; + /** + * @var string the path of the view file for rendering exceptions. + */ + public $exceptionView = '@yii/views/errorHandler/exception.php'; + /** + * @var string the path of the view file for rendering exceptions and errors call stack element. + */ + public $callStackItemView = '@yii/views/errorHandler/callStackItem.php'; + /** + * @var string the path of the view file for rendering previous exceptions. + */ + public $previousExceptionView = '@yii/views/errorHandler/previousException.php'; + /** + * @var \Exception the exception that is being handled currently. + */ + public $exception; + /** + * Handles exception. + * @param \Exception $exception to be handled. + */ + public function handle($exception) + { + $this->exception = $exception; + if ($this->discardExistingOutput) { + $this->clearOutput(); + } + $this->renderException($exception); + } - /** - * Handles exception. - * @param \Exception $exception to be handled. - */ - public function handle($exception) - { - $this->exception = $exception; - if ($this->discardExistingOutput) { - $this->clearOutput(); - } - $this->renderException($exception); - } + /** + * Renders the exception. + * @param \Exception $exception the exception to be handled. + */ + protected function renderException($exception) + { + if (Yii::$app instanceof \yii\console\Application || YII_ENV_TEST) { + echo Yii::$app->renderException($exception); + if (!YII_ENV_TEST) { + exit(1); + } - /** - * Renders the exception. - * @param \Exception $exception the exception to be handled. - */ - protected function renderException($exception) - { - if (Yii::$app instanceof \yii\console\Application || YII_ENV_TEST) { - echo Yii::$app->renderException($exception); - if (!YII_ENV_TEST) { - exit(1); - } - return; - } + return; + } - $response = Yii::$app->getResponse(); + $response = Yii::$app->getResponse(); - $useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException); + $useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException); - if ($useErrorView && $this->errorAction !== null) { - $result = Yii::$app->runAction($this->errorAction); - if ($result instanceof Response) { - $response = $result; - } else { - $response->data = $result; - } - } elseif ($response->format === \yii\web\Response::FORMAT_HTML) { - if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { - // AJAX request - $response->data = Yii::$app->renderException($exception); - } else { - // if there is an error during error rendering it's useful to - // display PHP error in debug mode instead of a blank screen - if (YII_DEBUG) { - ini_set('display_errors', 1); - } - $file = $useErrorView ? $this->errorView : $this->exceptionView; - $response->data = $this->renderFile($file, [ - 'exception' => $exception, - ]); - } - } else { - $response->data = $this->convertExceptionToArray($exception); - } + if ($useErrorView && $this->errorAction !== null) { + $result = Yii::$app->runAction($this->errorAction); + if ($result instanceof Response) { + $response = $result; + } else { + $response->data = $result; + } + } elseif ($response->format === \yii\web\Response::FORMAT_HTML) { + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { + // AJAX request + $response->data = Yii::$app->renderException($exception); + } else { + // if there is an error during error rendering it's useful to + // display PHP error in debug mode instead of a blank screen + if (YII_DEBUG) { + ini_set('display_errors', 1); + } + $file = $useErrorView ? $this->errorView : $this->exceptionView; + $response->data = $this->renderFile($file, [ + 'exception' => $exception, + ]); + } + } else { + $response->data = $this->convertExceptionToArray($exception); + } - if ($exception instanceof HttpException) { - $response->setStatusCode($exception->statusCode); - } else { - $response->setStatusCode(500); - } + if ($exception instanceof HttpException) { + $response->setStatusCode($exception->statusCode); + } else { + $response->setStatusCode(500); + } - $response->send(); - } + $response->send(); + } - /** - * Converts an exception into an array. - * @param \Exception $exception the exception being converted - * @return array the array representation of the exception. - */ - protected function convertExceptionToArray($exception) - { - $array = [ - 'type' => get_class($exception), - 'name' => $exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : 'Exception', - 'message' => $exception->getMessage(), - 'code' => $exception->getCode(), - ]; - if ($exception instanceof HttpException) { - $array['status'] = $exception->statusCode; - } - if (($prev = $exception->getPrevious()) !== null) { - $array['previous'] = $this->convertExceptionToArray($prev); - } - return $array; - } + /** + * Converts an exception into an array. + * @param \Exception $exception the exception being converted + * @return array the array representation of the exception. + */ + protected function convertExceptionToArray($exception) + { + $array = [ + 'type' => get_class($exception), + 'name' => $exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : 'Exception', + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + ]; + if ($exception instanceof HttpException) { + $array['status'] = $exception->statusCode; + } + if (($prev = $exception->getPrevious()) !== null) { + $array['previous'] = $this->convertExceptionToArray($prev); + } - /** - * Converts special characters to HTML entities. - * @param string $text to encode. - * @return string encoded original text. - */ - public function htmlEncode($text) - { - return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset); - } + return $array; + } - /** - * Removes all output echoed before calling this method. - */ - public function clearOutput() - { - // the following manual level counting is to deal with zlib.output_compression set to On - for ($level = ob_get_level(); $level > 0; --$level) { - if (!@ob_end_clean()) { - ob_clean(); - } - } - } + /** + * Converts special characters to HTML entities. + * @param string $text to encode. + * @return string encoded original text. + */ + public function htmlEncode($text) + { + return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset); + } - /** - * Adds informational links to the given PHP type/class. - * @param string $code type/class name to be linkified. - * @return string linkified with HTML type/class name. - */ - public function addTypeLinks($code) - { - $html = ''; - if (strpos($code, '\\') !== false) { - // namespaced class - foreach (explode('\\', $code) as $part) { - $html .= '' . $this->htmlEncode($part) . '\\'; - } - $html = rtrim($html, '\\'); - } elseif (strpos($code, '()') !== false) { - // method/function call - $html = preg_replace_callback('/^(.*)\(\)$/', function ($matches) { - return '' . - $this->htmlEncode($matches[1]) . '()'; - }, $code); - } - return $html; - } + /** + * Removes all output echoed before calling this method. + */ + public function clearOutput() + { + // the following manual level counting is to deal with zlib.output_compression set to On + for ($level = ob_get_level(); $level > 0; --$level) { + if (!@ob_end_clean()) { + ob_clean(); + } + } + } - /** - * Renders a view file as a PHP script. - * @param string $_file_ the view file. - * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. - * @return string the rendering result - */ - public function renderFile($_file_, $_params_) - { - $_params_['handler'] = $this; - if ($this->exception instanceof ErrorException) { - ob_start(); - ob_implicit_flush(false); - extract($_params_, EXTR_OVERWRITE); - require(Yii::getAlias($_file_)); - return ob_get_clean(); - } else { - return Yii::$app->getView()->renderFile($_file_, $_params_, $this); - } - } + /** + * Adds informational links to the given PHP type/class. + * @param string $code type/class name to be linkified. + * @return string linkified with HTML type/class name. + */ + public function addTypeLinks($code) + { + $html = ''; + if (strpos($code, '\\') !== false) { + // namespaced class + foreach (explode('\\', $code) as $part) { + $html .= '' . $this->htmlEncode($part) . '\\'; + } + $html = rtrim($html, '\\'); + } elseif (strpos($code, '()') !== false) { + // method/function call + $html = preg_replace_callback('/^(.*)\(\)$/', function ($matches) { + return '' . + $this->htmlEncode($matches[1]) . '()'; + }, $code); + } - /** - * Renders the previous exception stack for a given Exception. - * @param \Exception $exception the exception whose precursors should be rendered. - * @return string HTML content of the rendered previous exceptions. - * Empty string if there are none. - */ - public function renderPreviousExceptions($exception) - { - if (($previous = $exception->getPrevious()) !== null) { - return $this->renderFile($this->previousExceptionView, ['exception' => $previous]); - } else { - return ''; - } - } + return $html; + } - /** - * Renders a single call stack element. - * @param string|null $file name where call has happened. - * @param integer|null $line number on which call has happened. - * @param string|null $class called class name. - * @param string|null $method called function/method name. - * @param integer $index number of the call stack element. - * @return string HTML content of the rendered call stack element. - */ - public function renderCallStackItem($file, $line, $class, $method, $index) - { - $lines = []; - $begin = $end = 0; - if ($file !== null && $line !== null) { - $line--; // adjust line number from one-based to zero-based - $lines = @file($file); - if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) { - return ''; - } + /** + * Renders a view file as a PHP script. + * @param string $_file_ the view file. + * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. + * @return string the rendering result + */ + public function renderFile($_file_, $_params_) + { + $_params_['handler'] = $this; + if ($this->exception instanceof ErrorException) { + ob_start(); + ob_implicit_flush(false); + extract($_params_, EXTR_OVERWRITE); + require(Yii::getAlias($_file_)); - $half = (int)(($index == 0 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2); - $begin = $line - $half > 0 ? $line - $half : 0; - $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1; - } + return ob_get_clean(); + } else { + return Yii::$app->getView()->renderFile($_file_, $_params_, $this); + } + } - return $this->renderFile($this->callStackItemView, [ - 'file' => $file, - 'line' => $line, - 'class' => $class, - 'method' => $method, - 'index' => $index, - 'lines' => $lines, - 'begin' => $begin, - 'end' => $end, - ]); - } + /** + * Renders the previous exception stack for a given Exception. + * @param \Exception $exception the exception whose precursors should be rendered. + * @return string HTML content of the rendered previous exceptions. + * Empty string if there are none. + */ + public function renderPreviousExceptions($exception) + { + if (($previous = $exception->getPrevious()) !== null) { + return $this->renderFile($this->previousExceptionView, ['exception' => $previous]); + } else { + return ''; + } + } - /** - * Renders the request information. - * @return string the rendering result - */ - public function renderRequest() - { - $request = ''; - foreach (['_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV'] as $name) { - if (!empty($GLOBALS[$name])) { - $request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n"; - } - } - return '
    ' . rtrim($request, "\n") . '
    '; - } + /** + * Renders a single call stack element. + * @param string|null $file name where call has happened. + * @param integer|null $line number on which call has happened. + * @param string|null $class called class name. + * @param string|null $method called function/method name. + * @param integer $index number of the call stack element. + * @return string HTML content of the rendered call stack element. + */ + public function renderCallStackItem($file, $line, $class, $method, $index) + { + $lines = []; + $begin = $end = 0; + if ($file !== null && $line !== null) { + $line--; // adjust line number from one-based to zero-based + $lines = @file($file); + if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) { + return ''; + } - /** - * Determines whether given name of the file belongs to the framework. - * @param string $file name to be checked. - * @return boolean whether given name of the file belongs to the framework. - */ - public function isCoreFile($file) - { - return $file === null || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0; - } + $half = (int) (($index == 0 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2); + $begin = $line - $half > 0 ? $line - $half : 0; + $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1; + } - /** - * Creates HTML containing link to the page with the information on given HTTP status code. - * @param integer $statusCode to be used to generate information link. - * @param string $statusDescription Description to display after the the status code. - * @return string generated HTML with HTTP status code information. - */ - public function createHttpStatusLink($statusCode, $statusDescription) - { - return 'HTTP ' . (int)$statusCode . ' – ' . $statusDescription . ''; - } + return $this->renderFile($this->callStackItemView, [ + 'file' => $file, + 'line' => $line, + 'class' => $class, + 'method' => $method, + 'index' => $index, + 'lines' => $lines, + 'begin' => $begin, + 'end' => $end, + ]); + } - /** - * Creates string containing HTML link which refers to the home page of determined web-server software - * and its full name. - * @return string server software information hyperlink. - */ - public function createServerInformationLink() - { - static $serverUrls = [ - 'http://httpd.apache.org/' => ['apache'], - 'http://nginx.org/' => ['nginx'], - 'http://lighttpd.net/' => ['lighttpd'], - 'http://gwan.com/' => ['g-wan', 'gwan'], - 'http://iis.net/' => ['iis', 'services'], - 'http://php.net/manual/en/features.commandline.webserver.php' => ['development'], - ]; - if (isset($_SERVER['SERVER_SOFTWARE'])) { - foreach ($serverUrls as $url => $keywords) { - foreach ($keywords as $keyword) { - if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) { - return '' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . ''; - } - } - } - } - return ''; - } + /** + * Renders the request information. + * @return string the rendering result + */ + public function renderRequest() + { + $request = ''; + foreach (['_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV'] as $name) { + if (!empty($GLOBALS[$name])) { + $request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n"; + } + } - /** - * Creates string containing HTML link which refers to the page with the current version - * of the framework and version number text. - * @return string framework version information hyperlink. - */ - public function createFrameworkVersionLink() - { - return '' . $this->htmlEncode(Yii::getVersion()) . ''; - } + return '
    ' . rtrim($request, "\n") . '
    '; + } + + /** + * Determines whether given name of the file belongs to the framework. + * @param string $file name to be checked. + * @return boolean whether given name of the file belongs to the framework. + */ + public function isCoreFile($file) + { + return $file === null || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0; + } + + /** + * Creates HTML containing link to the page with the information on given HTTP status code. + * @param integer $statusCode to be used to generate information link. + * @param string $statusDescription Description to display after the the status code. + * @return string generated HTML with HTTP status code information. + */ + public function createHttpStatusLink($statusCode, $statusDescription) + { + return 'HTTP ' . (int) $statusCode . ' – ' . $statusDescription . ''; + } + + /** + * Creates string containing HTML link which refers to the home page of determined web-server software + * and its full name. + * @return string server software information hyperlink. + */ + public function createServerInformationLink() + { + static $serverUrls = [ + 'http://httpd.apache.org/' => ['apache'], + 'http://nginx.org/' => ['nginx'], + 'http://lighttpd.net/' => ['lighttpd'], + 'http://gwan.com/' => ['g-wan', 'gwan'], + 'http://iis.net/' => ['iis', 'services'], + 'http://php.net/manual/en/features.commandline.webserver.php' => ['development'], + ]; + if (isset($_SERVER['SERVER_SOFTWARE'])) { + foreach ($serverUrls as $url => $keywords) { + foreach ($keywords as $keyword) { + if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) { + return '' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . ''; + } + } + } + } + + return ''; + } + + /** + * Creates string containing HTML link which refers to the page with the current version + * of the framework and version number text. + * @return string framework version information hyperlink. + */ + public function createFrameworkVersionLink() + { + return '' . $this->htmlEncode(Yii::getVersion()) . ''; + } } diff --git a/framework/base/Event.php b/framework/base/Event.php index 1a83467daf8..def96818540 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -24,162 +24,165 @@ */ class Event extends Object { - /** - * @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]]. - * Event handlers may use this property to check what event it is handling. - */ - public $name; - /** - * @var object the sender of this event. If not set, this property will be - * set as the object whose "trigger()" method is called. - * This property may also be a `null` when this event is a - * class-level event which is triggered in a static context. - */ - public $sender; - /** - * @var boolean whether the event is handled. Defaults to false. - * When a handler sets this to be true, the event processing will stop and - * ignore the rest of the uninvoked event handlers. - */ - public $handled = false; - /** - * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler. - * Note that this varies according to which event handler is currently executing. - */ - public $data; + /** + * @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]]. + * Event handlers may use this property to check what event it is handling. + */ + public $name; + /** + * @var object the sender of this event. If not set, this property will be + * set as the object whose "trigger()" method is called. + * This property may also be a `null` when this event is a + * class-level event which is triggered in a static context. + */ + public $sender; + /** + * @var boolean whether the event is handled. Defaults to false. + * When a handler sets this to be true, the event processing will stop and + * ignore the rest of the uninvoked event handlers. + */ + public $handled = false; + /** + * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler. + * Note that this varies according to which event handler is currently executing. + */ + public $data; - private static $_events = []; + private static $_events = []; - /** - * Attaches an event handler to a class-level event. - * - * When a class-level event is triggered, event handlers attached - * to that class and all parent classes will be invoked. - * - * For example, the following code attaches an event handler to `ActiveRecord`'s - * `afterInsert` event: - * - * ~~~ - * Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) { - * Yii::trace(get_class($event->sender) . ' is inserted.'); - * }); - * ~~~ - * - * The handler will be invoked for EVERY successful ActiveRecord insertion. - * - * For more details about how to declare an event handler, please refer to [[Component::on()]]. - * - * @param string $class the fully qualified class name to which the event handler needs to attach. - * @param string $name the event name. - * @param callable $handler the event handler. - * @param mixed $data the data to be passed to the event handler when the event is triggered. - * When the event handler is invoked, this data can be accessed via [[Event::data]]. - * @see off() - */ - public static function on($class, $name, $handler, $data = null) - { - self::$_events[$name][ltrim($class, '\\')][] = [$handler, $data]; - } + /** + * Attaches an event handler to a class-level event. + * + * When a class-level event is triggered, event handlers attached + * to that class and all parent classes will be invoked. + * + * For example, the following code attaches an event handler to `ActiveRecord`'s + * `afterInsert` event: + * + * ~~~ + * Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) { + * Yii::trace(get_class($event->sender) . ' is inserted.'); + * }); + * ~~~ + * + * The handler will be invoked for EVERY successful ActiveRecord insertion. + * + * For more details about how to declare an event handler, please refer to [[Component::on()]]. + * + * @param string $class the fully qualified class name to which the event handler needs to attach. + * @param string $name the event name. + * @param callable $handler the event handler. + * @param mixed $data the data to be passed to the event handler when the event is triggered. + * When the event handler is invoked, this data can be accessed via [[Event::data]]. + * @see off() + */ + public static function on($class, $name, $handler, $data = null) + { + self::$_events[$name][ltrim($class, '\\')][] = [$handler, $data]; + } - /** - * Detaches an event handler from a class-level event. - * - * This method is the opposite of [[on()]]. - * - * @param string $class the fully qualified class name from which the event handler needs to be detached. - * @param string $name the event name. - * @param callable $handler the event handler to be removed. - * If it is null, all handlers attached to the named event will be removed. - * @return boolean whether a handler is found and detached. - * @see on() - */ - public static function off($class, $name, $handler = null) - { - $class = ltrim($class, '\\'); - if (empty(self::$_events[$name][$class])) { - return false; - } - if ($handler === null) { - unset(self::$_events[$name][$class]); - return true; - } else { - $removed = false; - foreach (self::$_events[$name][$class] as $i => $event) { - if ($event[0] === $handler) { - unset(self::$_events[$name][$class][$i]); - $removed = true; - } - } - if ($removed) { - self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); - } - return $removed; - } - } + /** + * Detaches an event handler from a class-level event. + * + * This method is the opposite of [[on()]]. + * + * @param string $class the fully qualified class name from which the event handler needs to be detached. + * @param string $name the event name. + * @param callable $handler the event handler to be removed. + * If it is null, all handlers attached to the named event will be removed. + * @return boolean whether a handler is found and detached. + * @see on() + */ + public static function off($class, $name, $handler = null) + { + $class = ltrim($class, '\\'); + if (empty(self::$_events[$name][$class])) { + return false; + } + if ($handler === null) { + unset(self::$_events[$name][$class]); - /** - * Returns a value indicating whether there is any handler attached to the specified class-level event. - * Note that this method will also check all parent classes to see if there is any handler attached - * to the named event. - * @param string|object $class the object or the fully qualified class name specifying the class-level event. - * @param string $name the event name. - * @return boolean whether there is any handler attached to the event. - */ - public static function hasHandlers($class, $name) - { - if (empty(self::$_events[$name])) { - return false; - } - if (is_object($class)) { - $class = get_class($class); - } else { - $class = ltrim($class, '\\'); - } - do { - if (!empty(self::$_events[$name][$class])) { - return true; - } - } while (($class = get_parent_class($class)) !== false); - return false; - } + return true; + } else { + $removed = false; + foreach (self::$_events[$name][$class] as $i => $event) { + if ($event[0] === $handler) { + unset(self::$_events[$name][$class][$i]); + $removed = true; + } + } + if ($removed) { + self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); + } - /** - * Triggers a class-level event. - * This method will cause invocation of event handlers that are attached to the named event - * for the specified class and all its parent classes. - * @param string|object $class the object or the fully qualified class name specifying the class-level event. - * @param string $name the event name. - * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. - */ - public static function trigger($class, $name, $event = null) - { - if (empty(self::$_events[$name])) { - return; - } - if ($event === null) { - $event = new static; - } - $event->handled = false; - $event->name = $name; + return $removed; + } + } - if (is_object($class)) { - if ($event->sender === null) { - $event->sender = $class; - } - $class = get_class($class); - } else { - $class = ltrim($class, '\\'); - } - do { - if (!empty(self::$_events[$name][$class])) { - foreach (self::$_events[$name][$class] as $handler) { - $event->data = $handler[1]; - call_user_func($handler[0], $event); - if ($event->handled) { - return; - } - } - } - } while (($class = get_parent_class($class)) !== false); - } + /** + * Returns a value indicating whether there is any handler attached to the specified class-level event. + * Note that this method will also check all parent classes to see if there is any handler attached + * to the named event. + * @param string|object $class the object or the fully qualified class name specifying the class-level event. + * @param string $name the event name. + * @return boolean whether there is any handler attached to the event. + */ + public static function hasHandlers($class, $name) + { + if (empty(self::$_events[$name])) { + return false; + } + if (is_object($class)) { + $class = get_class($class); + } else { + $class = ltrim($class, '\\'); + } + do { + if (!empty(self::$_events[$name][$class])) { + return true; + } + } while (($class = get_parent_class($class)) !== false); + + return false; + } + + /** + * Triggers a class-level event. + * This method will cause invocation of event handlers that are attached to the named event + * for the specified class and all its parent classes. + * @param string|object $class the object or the fully qualified class name specifying the class-level event. + * @param string $name the event name. + * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. + */ + public static function trigger($class, $name, $event = null) + { + if (empty(self::$_events[$name])) { + return; + } + if ($event === null) { + $event = new static; + } + $event->handled = false; + $event->name = $name; + + if (is_object($class)) { + if ($event->sender === null) { + $event->sender = $class; + } + $class = get_class($class); + } else { + $class = ltrim($class, '\\'); + } + do { + if (!empty(self::$_events[$name][$class])) { + foreach (self::$_events[$name][$class] as $handler) { + $event->data = $handler[1]; + call_user_func($handler[0], $event); + if ($event->handled) { + return; + } + } + } + } while (($class = get_parent_class($class)) !== false); + } } diff --git a/framework/base/Exception.php b/framework/base/Exception.php index 77526f37f26..e12cf1b9f06 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -15,11 +15,11 @@ */ class Exception extends \Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Exception'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Exception'; + } } diff --git a/framework/base/Extension.php b/framework/base/Extension.php index c25a0430b8e..0cbc0cc7be2 100644 --- a/framework/base/Extension.php +++ b/framework/base/Extension.php @@ -19,11 +19,11 @@ */ class Extension { - /** - * Initializes the extension. - * This method is invoked at the end of [[Application::init()]]. - */ - public static function init() - { - } + /** + * Initializes the extension. + * This method is invoked at the end of [[Application::init()]]. + */ + public static function init() + { + } } diff --git a/framework/base/Formatter.php b/framework/base/Formatter.php index 8888d761c7a..0e1dd6b3c3c 100644 --- a/framework/base/Formatter.php +++ b/framework/base/Formatter.php @@ -27,432 +27,447 @@ */ class Formatter extends Component { - /** - * @var string the timezone to use for formatting time and date values. - * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php) - * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`. - * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones. - * If this property is not set, [[\yii\base\Application::timezone]] will be used. - */ - public $timeZone; - /** - * @var string the default format string to be used to format a date using PHP date() function. - */ - public $dateFormat = 'Y/m/d'; - /** - * @var string the default format string to be used to format a time using PHP date() function. - */ - public $timeFormat = 'h:i:s A'; - /** - * @var string the default format string to be used to format a date and time using PHP date() function. - */ - public $datetimeFormat = 'Y/m/d h:i:s A'; - /** - * @var string the text to be displayed when formatting a null. Defaults to '(not set)'. - */ - public $nullDisplay; - /** - * @var array the text to be displayed when formatting a boolean value. The first element corresponds - * to the text display for false, the second element for true. Defaults to `['No', 'Yes']`. - */ - public $booleanFormat; - /** - * @var string the character displayed as the decimal point when formatting a number. - * If not set, "." will be used. - */ - public $decimalSeparator; - /** - * @var string the character displayed as the thousands separator character when formatting a number. - * If not set, "," will be used. - */ - public $thousandSeparator; - /** - * @var array the format used to format size (bytes). Three elements may be specified: "base", "decimals" and "decimalSeparator". - * They correspond to the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte, defaults to 1024), - * the number of digits after the decimal point (defaults to 2) and the character displayed as the decimal point. - */ - public $sizeFormat = [ - 'base' => 1024, - 'decimals' => 2, - 'decimalSeparator' => null, - ]; - - /** - * Initializes the component. - */ - public function init() - { - if ($this->timeZone === null) { - $this->timeZone = Yii::$app->timeZone; - } - - if (empty($this->booleanFormat)) { - $this->booleanFormat = [Yii::t('yii', 'No'), Yii::t('yii', 'Yes')]; - } - if ($this->nullDisplay === null) { - $this->nullDisplay = '' . Yii::t('yii', '(not set)') . ''; - } - } - - /** - * Formats the value based on the given format type. - * This method will call one of the "as" methods available in this class to do the formatting. - * For type "xyz", the method "asXyz" will be used. For example, if the format is "html", - * then [[asHtml()]] will be used. Format names are case insensitive. - * @param mixed $value the value to be formatted - * @param string|array $format the format of the value, e.g., "html", "text". To specify additional - * parameters of the formatting method, you may use an array. The first element of the array - * specifies the format name, while the rest of the elements will be used as the parameters to the formatting - * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`. - * @return string the formatting result - * @throws InvalidParamException if the type is not supported by this class. - */ - public function format($value, $format) - { - if (is_array($format)) { - if (!isset($format[0])) { - throw new InvalidParamException('The $format array must contain at least one element.'); - } - $f = $format[0]; - $format[0] = $value; - $params = $format; - $format = $f; - } else { - $params = [$value]; - } - $method = 'as' . $format; - if ($this->hasMethod($method)) { - return call_user_func_array([$this, $method], $params); - } else { - throw new InvalidParamException("Unknown type: $format"); - } - } - - /** - * Formats the value as is without any formatting. - * This method simply returns back the parameter without any format. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asRaw($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return $value; - } - - /** - * Formats the value as an HTML-encoded plain text. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asText($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return Html::encode($value); - } - - /** - * Formats the value as an HTML-encoded plain text with newlines converted into breaks. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asNtext($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return nl2br(Html::encode($value)); - } - - /** - * Formats the value as HTML-encoded text paragraphs. - * Each text paragraph is enclosed within a `

    ` tag. - * One or multiple consecutive empty lines divide two paragraphs. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asParagraphs($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return str_replace('

    ', '', - '

    ' . preg_replace('/[\r\n]{2,}/', "

    \n

    ", Html::encode($value)) . '

    ' - ); - } - - /** - * Formats the value as HTML text. - * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks. - * Use [[asRaw()]] if you do not want any purification of the value. - * @param mixed $value the value to be formatted - * @param array|null $config the configuration for the HTMLPurifier class. - * @return string the formatted result - */ - public function asHtml($value, $config = null) - { - if ($value === null) { - return $this->nullDisplay; - } - return HtmlPurifier::process($value, $config); - } - - /** - * Formats the value as a mailto link. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asEmail($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return Html::mailto(Html::encode($value), $value); - } - - /** - * Formats the value as an image tag. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asImage($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return Html::img($value); - } - - /** - * Formats the value as a hyperlink. - * @param mixed $value the value to be formatted - * @return string the formatted result - */ - public function asUrl($value) - { - if ($value === null) { - return $this->nullDisplay; - } - $url = $value; - if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { - $url = 'http://' . $url; - } - return Html::a(Html::encode($value), $url); - } - - /** - * Formats the value as a boolean. - * @param mixed $value the value to be formatted - * @return string the formatted result - * @see booleanFormat - */ - public function asBoolean($value) - { - if ($value === null) { - return $this->nullDisplay; - } - return $value ? $this->booleanFormat[1] : $this->booleanFormat[0]; - } - - /** - * Formats the value as a date. - * @param integer|string|DateTime $value the value to be formatted. The following - * types of value are supported: - * - * - an integer representing a UNIX timestamp - * - a string that can be parsed into a UNIX timestamp via `strtotime()` - * - a PHP DateTime object - * - * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. The format string should be one - * that can be recognized by the PHP `date()` function. - * @return string the formatted result - * @see dateFormat - */ - public function asDate($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - $value = $this->normalizeDatetimeValue($value); - return $this->formatTimestamp($value, $format === null ? $this->dateFormat : $format); - } - - /** - * Formats the value as a time. - * @param integer|string|DateTime $value the value to be formatted. The following - * types of value are supported: - * - * - an integer representing a UNIX timestamp - * - a string that can be parsed into a UNIX timestamp via `strtotime()` - * - a PHP DateTime object - * - * @param string $format the format used to convert the value into a date string. - * If null, [[timeFormat]] will be used. The format string should be one - * that can be recognized by the PHP `date()` function. - * @return string the formatted result - * @see timeFormat - */ - public function asTime($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - $value = $this->normalizeDatetimeValue($value); - return $this->formatTimestamp($value, $format === null ? $this->timeFormat : $format); - } - - /** - * Formats the value as a datetime. - * @param integer|string|DateTime $value the value to be formatted. The following - * types of value are supported: - * - * - an integer representing a UNIX timestamp - * - a string that can be parsed into a UNIX timestamp via `strtotime()` - * - a PHP DateTime object - * - * @param string $format the format used to convert the value into a date string. - * If null, [[datetimeFormat]] will be used. The format string should be one - * that can be recognized by the PHP `date()` function. - * @return string the formatted result - * @see datetimeFormat - */ - public function asDatetime($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - $value = $this->normalizeDatetimeValue($value); - return $this->formatTimestamp($value, $format === null ? $this->datetimeFormat : $format); - } - - /** - * Normalizes the given datetime value as one that can be taken by various date/time formatting methods. - * @param mixed $value the datetime value to be normalized. - * @return integer the normalized datetime value - */ - protected function normalizeDatetimeValue($value) - { - if (is_string($value)) { - return is_numeric($value) || $value === '' ? (int)$value : strtotime($value); - } elseif ($value instanceof DateTime || $value instanceof \DateTimeInterface) { - return $value->getTimestamp(); - } else { - return (int)$value; - } - } - - /** - * @param integer $value normalized datetime value - * @param string $format the format used to convert the value into a date string. - * @return string the formatted result - */ - protected function formatTimestamp($value, $format) - { - $date = new DateTime(null, new \DateTimeZone($this->timeZone)); - $date->setTimestamp($value); - return $date->format($format); - } - - /** - * Formats the value as an integer. - * @param mixed $value the value to be formatted - * @return string the formatting result. - */ - public function asInteger($value) - { - if ($value === null) { - return $this->nullDisplay; - } - if (is_string($value) && preg_match('/^(-?\d+)/', $value, $matches)) { - return $matches[1]; - } else { - $value = (int)$value; - return "$value"; - } - } - - /** - * Formats the value as a double number. - * Property [[decimalSeparator]] will be used to represent the decimal point. - * @param mixed $value the value to be formatted - * @param integer $decimals the number of digits after the decimal point - * @return string the formatting result. - * @see decimalSeparator - */ - public function asDouble($value, $decimals = 2) - { - if ($value === null) { - return $this->nullDisplay; - } - if ($this->decimalSeparator === null) { - return sprintf("%.{$decimals}f", $value); - } else { - return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value)); - } - } - - /** - * Formats the value as a number with decimal and thousand separators. - * This method calls the PHP number_format() function to do the formatting. - * @param mixed $value the value to be formatted - * @param integer $decimals the number of digits after the decimal point - * @return string the formatted result - * @see decimalSeparator - * @see thousandSeparator - */ - public function asNumber($value, $decimals = 0) - { - if ($value === null) { - return $this->nullDisplay; - } - $ds = isset($this->decimalSeparator) ? $this->decimalSeparator: '.'; - $ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ','; - return number_format($value, $decimals, $ds, $ts); - } - - /** - * Formats the value in bytes as a size in human readable form. - * @param integer $value value in bytes to be formatted - * @param boolean $verbose if full names should be used (e.g. bytes, kilobytes, ...). - * Defaults to false meaning that short names will be used (e.g. B, KB, ...). - * @return string the formatted result - * @see sizeFormat - */ - public function asSize($value, $verbose = false) - { - $position = 0; - - do { - if ($value < $this->sizeFormat['base']) { - break; - } - - $value = $value / $this->sizeFormat['base']; - $position++; - } while ($position < 6); - - $value = round($value, $this->sizeFormat['decimals']); - $formattedValue = isset($this->sizeFormat['decimalSeparator']) ? str_replace('.', $this->sizeFormat['decimalSeparator'], $value) : $value; - $params = ['n' => $formattedValue]; - - switch($position) { - case 0: - return $verbose ? Yii::t('yii', '{n, plural, =1{# byte} other{# bytes}}', $params) : Yii::t('yii', '{n} B', $params); - case 1: - return $verbose ? Yii::t('yii', '{n, plural, =1{# kilobyte} other{# kilobytes}}', $params) : Yii::t('yii', '{n} KB', $params); - case 2: - return $verbose ? Yii::t('yii', '{n, plural, =1{# megabyte} other{# megabytes}}', $params) : Yii::t('yii', '{n} MB', $params); - case 3: - return $verbose ? Yii::t('yii', '{n, plural, =1{# gigabyte} other{# gigabytes}}', $params) : Yii::t('yii', '{n} GB', $params); - case 4: - return $verbose ? Yii::t('yii', '{n, plural, =1{# terabyte} other{# terabytes}}', $params) : Yii::t('yii', '{n} TB', $params); - default: - return $verbose ? Yii::t('yii', '{n, plural, =1{# petabyte} other{# petabytes}}', $params) : Yii::t('yii', '{n} PB', $params); - } - } + /** + * @var string the timezone to use for formatting time and date values. + * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php) + * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`. + * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones. + * If this property is not set, [[\yii\base\Application::timezone]] will be used. + */ + public $timeZone; + /** + * @var string the default format string to be used to format a date using PHP date() function. + */ + public $dateFormat = 'Y/m/d'; + /** + * @var string the default format string to be used to format a time using PHP date() function. + */ + public $timeFormat = 'h:i:s A'; + /** + * @var string the default format string to be used to format a date and time using PHP date() function. + */ + public $datetimeFormat = 'Y/m/d h:i:s A'; + /** + * @var string the text to be displayed when formatting a null. Defaults to '(not set)'. + */ + public $nullDisplay; + /** + * @var array the text to be displayed when formatting a boolean value. The first element corresponds + * to the text display for false, the second element for true. Defaults to `['No', 'Yes']`. + */ + public $booleanFormat; + /** + * @var string the character displayed as the decimal point when formatting a number. + * If not set, "." will be used. + */ + public $decimalSeparator; + /** + * @var string the character displayed as the thousands separator character when formatting a number. + * If not set, "," will be used. + */ + public $thousandSeparator; + /** + * @var array the format used to format size (bytes). Three elements may be specified: "base", "decimals" and "decimalSeparator". + * They correspond to the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte, defaults to 1024), + * the number of digits after the decimal point (defaults to 2) and the character displayed as the decimal point. + */ + public $sizeFormat = [ + 'base' => 1024, + 'decimals' => 2, + 'decimalSeparator' => null, + ]; + + /** + * Initializes the component. + */ + public function init() + { + if ($this->timeZone === null) { + $this->timeZone = Yii::$app->timeZone; + } + + if (empty($this->booleanFormat)) { + $this->booleanFormat = [Yii::t('yii', 'No'), Yii::t('yii', 'Yes')]; + } + if ($this->nullDisplay === null) { + $this->nullDisplay = '' . Yii::t('yii', '(not set)') . ''; + } + } + + /** + * Formats the value based on the given format type. + * This method will call one of the "as" methods available in this class to do the formatting. + * For type "xyz", the method "asXyz" will be used. For example, if the format is "html", + * then [[asHtml()]] will be used. Format names are case insensitive. + * @param mixed $value the value to be formatted + * @param string|array $format the format of the value, e.g., "html", "text". To specify additional + * parameters of the formatting method, you may use an array. The first element of the array + * specifies the format name, while the rest of the elements will be used as the parameters to the formatting + * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`. + * @return string the formatting result + * @throws InvalidParamException if the type is not supported by this class. + */ + public function format($value, $format) + { + if (is_array($format)) { + if (!isset($format[0])) { + throw new InvalidParamException('The $format array must contain at least one element.'); + } + $f = $format[0]; + $format[0] = $value; + $params = $format; + $format = $f; + } else { + $params = [$value]; + } + $method = 'as' . $format; + if ($this->hasMethod($method)) { + return call_user_func_array([$this, $method], $params); + } else { + throw new InvalidParamException("Unknown type: $format"); + } + } + + /** + * Formats the value as is without any formatting. + * This method simply returns back the parameter without any format. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asRaw($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return $value; + } + + /** + * Formats the value as an HTML-encoded plain text. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asText($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return Html::encode($value); + } + + /** + * Formats the value as an HTML-encoded plain text with newlines converted into breaks. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asNtext($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return nl2br(Html::encode($value)); + } + + /** + * Formats the value as HTML-encoded text paragraphs. + * Each text paragraph is enclosed within a `

    ` tag. + * One or multiple consecutive empty lines divide two paragraphs. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asParagraphs($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return str_replace('

    ', '', + '

    ' . preg_replace('/[\r\n]{2,}/', "

    \n

    ", Html::encode($value)) . '

    ' + ); + } + + /** + * Formats the value as HTML text. + * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks. + * Use [[asRaw()]] if you do not want any purification of the value. + * @param mixed $value the value to be formatted + * @param array|null $config the configuration for the HTMLPurifier class. + * @return string the formatted result + */ + public function asHtml($value, $config = null) + { + if ($value === null) { + return $this->nullDisplay; + } + + return HtmlPurifier::process($value, $config); + } + + /** + * Formats the value as a mailto link. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asEmail($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return Html::mailto(Html::encode($value), $value); + } + + /** + * Formats the value as an image tag. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asImage($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return Html::img($value); + } + + /** + * Formats the value as a hyperlink. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asUrl($value) + { + if ($value === null) { + return $this->nullDisplay; + } + $url = $value; + if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { + $url = 'http://' . $url; + } + + return Html::a(Html::encode($value), $url); + } + + /** + * Formats the value as a boolean. + * @param mixed $value the value to be formatted + * @return string the formatted result + * @see booleanFormat + */ + public function asBoolean($value) + { + if ($value === null) { + return $this->nullDisplay; + } + + return $value ? $this->booleanFormat[1] : $this->booleanFormat[0]; + } + + /** + * Formats the value as a date. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see dateFormat + */ + public function asDate($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + + return $this->formatTimestamp($value, $format === null ? $this->dateFormat : $format); + } + + /** + * Formats the value as a time. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[timeFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see timeFormat + */ + public function asTime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + + return $this->formatTimestamp($value, $format === null ? $this->timeFormat : $format); + } + + /** + * Formats the value as a datetime. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[datetimeFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see datetimeFormat + */ + public function asDatetime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + + return $this->formatTimestamp($value, $format === null ? $this->datetimeFormat : $format); + } + + /** + * Normalizes the given datetime value as one that can be taken by various date/time formatting methods. + * @param mixed $value the datetime value to be normalized. + * @return integer the normalized datetime value + */ + protected function normalizeDatetimeValue($value) + { + if (is_string($value)) { + return is_numeric($value) || $value === '' ? (int) $value : strtotime($value); + } elseif ($value instanceof DateTime || $value instanceof \DateTimeInterface) { + return $value->getTimestamp(); + } else { + return (int) $value; + } + } + + /** + * @param integer $value normalized datetime value + * @param string $format the format used to convert the value into a date string. + * @return string the formatted result + */ + protected function formatTimestamp($value, $format) + { + $date = new DateTime(null, new \DateTimeZone($this->timeZone)); + $date->setTimestamp($value); + + return $date->format($format); + } + + /** + * Formats the value as an integer. + * @param mixed $value the value to be formatted + * @return string the formatting result. + */ + public function asInteger($value) + { + if ($value === null) { + return $this->nullDisplay; + } + if (is_string($value) && preg_match('/^(-?\d+)/', $value, $matches)) { + return $matches[1]; + } else { + $value = (int) $value; + + return "$value"; + } + } + + /** + * Formats the value as a double number. + * Property [[decimalSeparator]] will be used to represent the decimal point. + * @param mixed $value the value to be formatted + * @param integer $decimals the number of digits after the decimal point + * @return string the formatting result. + * @see decimalSeparator + */ + public function asDouble($value, $decimals = 2) + { + if ($value === null) { + return $this->nullDisplay; + } + if ($this->decimalSeparator === null) { + return sprintf("%.{$decimals}f", $value); + } else { + return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value)); + } + } + + /** + * Formats the value as a number with decimal and thousand separators. + * This method calls the PHP number_format() function to do the formatting. + * @param mixed $value the value to be formatted + * @param integer $decimals the number of digits after the decimal point + * @return string the formatted result + * @see decimalSeparator + * @see thousandSeparator + */ + public function asNumber($value, $decimals = 0) + { + if ($value === null) { + return $this->nullDisplay; + } + $ds = isset($this->decimalSeparator) ? $this->decimalSeparator: '.'; + $ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ','; + + return number_format($value, $decimals, $ds, $ts); + } + + /** + * Formats the value in bytes as a size in human readable form. + * @param integer $value value in bytes to be formatted + * @param boolean $verbose if full names should be used (e.g. bytes, kilobytes, ...). + * Defaults to false meaning that short names will be used (e.g. B, KB, ...). + * @return string the formatted result + * @see sizeFormat + */ + public function asSize($value, $verbose = false) + { + $position = 0; + + do { + if ($value < $this->sizeFormat['base']) { + break; + } + + $value = $value / $this->sizeFormat['base']; + $position++; + } while ($position < 6); + + $value = round($value, $this->sizeFormat['decimals']); + $formattedValue = isset($this->sizeFormat['decimalSeparator']) ? str_replace('.', $this->sizeFormat['decimalSeparator'], $value) : $value; + $params = ['n' => $formattedValue]; + + switch ($position) { + case 0: + return $verbose ? Yii::t('yii', '{n, plural, =1{# byte} other{# bytes}}', $params) : Yii::t('yii', '{n} B', $params); + case 1: + return $verbose ? Yii::t('yii', '{n, plural, =1{# kilobyte} other{# kilobytes}}', $params) : Yii::t('yii', '{n} KB', $params); + case 2: + return $verbose ? Yii::t('yii', '{n, plural, =1{# megabyte} other{# megabytes}}', $params) : Yii::t('yii', '{n} MB', $params); + case 3: + return $verbose ? Yii::t('yii', '{n, plural, =1{# gigabyte} other{# gigabytes}}', $params) : Yii::t('yii', '{n} GB', $params); + case 4: + return $verbose ? Yii::t('yii', '{n, plural, =1{# terabyte} other{# terabytes}}', $params) : Yii::t('yii', '{n} TB', $params); + default: + return $verbose ? Yii::t('yii', '{n, plural, =1{# petabyte} other{# petabytes}}', $params) : Yii::t('yii', '{n} PB', $params); + } + } } diff --git a/framework/base/InlineAction.php b/framework/base/InlineAction.php index 412b357a584..a3ef32af604 100644 --- a/framework/base/InlineAction.php +++ b/framework/base/InlineAction.php @@ -20,36 +20,37 @@ */ class InlineAction extends Action { - /** - * @var string the controller method that this inline action is associated with - */ - public $actionMethod; + /** + * @var string the controller method that this inline action is associated with + */ + public $actionMethod; - /** - * @param string $id the ID of this action - * @param Controller $controller the controller that owns this action - * @param string $actionMethod the controller method that this inline action is associated with - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $controller, $actionMethod, $config = []) - { - $this->actionMethod = $actionMethod; - parent::__construct($id, $controller, $config); - } + /** + * @param string $id the ID of this action + * @param Controller $controller the controller that owns this action + * @param string $actionMethod the controller method that this inline action is associated with + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $controller, $actionMethod, $config = []) + { + $this->actionMethod = $actionMethod; + parent::__construct($id, $controller, $config); + } - /** - * Runs this action with the specified parameters. - * This method is mainly invoked by the controller. - * @param array $params action parameters - * @return mixed the result of the action - */ - public function runWithParams($params) - { - $args = $this->controller->bindActionParams($this, $params); - Yii::trace('Running action: ' . get_class($this->controller) . '::' . $this->actionMethod . '()', __METHOD__); - if (Yii::$app->requestedParams === null) { - Yii::$app->requestedParams = $args; - } - return call_user_func_array([$this->controller, $this->actionMethod], $args); - } + /** + * Runs this action with the specified parameters. + * This method is mainly invoked by the controller. + * @param array $params action parameters + * @return mixed the result of the action + */ + public function runWithParams($params) + { + $args = $this->controller->bindActionParams($this, $params); + Yii::trace('Running action: ' . get_class($this->controller) . '::' . $this->actionMethod . '()', __METHOD__); + if (Yii::$app->requestedParams === null) { + Yii::$app->requestedParams = $args; + } + + return call_user_func_array([$this->controller, $this->actionMethod], $args); + } } diff --git a/framework/base/InvalidCallException.php b/framework/base/InvalidCallException.php index bc46ca46fa3..fad85e1976a 100644 --- a/framework/base/InvalidCallException.php +++ b/framework/base/InvalidCallException.php @@ -15,11 +15,11 @@ */ class InvalidCallException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Invalid Call'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Invalid Call'; + } } diff --git a/framework/base/InvalidConfigException.php b/framework/base/InvalidConfigException.php index 1fb0b4aef4e..953675f9303 100644 --- a/framework/base/InvalidConfigException.php +++ b/framework/base/InvalidConfigException.php @@ -15,11 +15,11 @@ */ class InvalidConfigException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Invalid Configuration'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Invalid Configuration'; + } } diff --git a/framework/base/InvalidParamException.php b/framework/base/InvalidParamException.php index b2f919185ab..62c5f815caf 100644 --- a/framework/base/InvalidParamException.php +++ b/framework/base/InvalidParamException.php @@ -15,11 +15,11 @@ */ class InvalidParamException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Invalid Parameter'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Invalid Parameter'; + } } diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php index b94fab684c2..43bed6ef923 100644 --- a/framework/base/InvalidRouteException.php +++ b/framework/base/InvalidRouteException.php @@ -15,11 +15,11 @@ */ class InvalidRouteException extends UserException { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Invalid Route'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Invalid Route'; + } } diff --git a/framework/base/MailEvent.php b/framework/base/MailEvent.php index 758bd9044f1..762b9597577 100644 --- a/framework/base/MailEvent.php +++ b/framework/base/MailEvent.php @@ -18,18 +18,18 @@ class MailEvent extends Event { - /** - * @var \yii\mail\MessageInterface mail message being send - */ - public $message; - /** - * @var boolean if message send was successful - */ - public $isSuccessful; - /** - * @var boolean whether to continue sending an email. Event handlers of - * [[\yii\mail\BaseMailer::EVENT_BEFORE_SEND]] may set this property to decide whether - * to continue send or not. - */ - public $isValid = true; + /** + * @var \yii\mail\MessageInterface mail message being send + */ + public $message; + /** + * @var boolean if message send was successful + */ + public $isSuccessful; + /** + * @var boolean whether to continue sending an email. Event handlers of + * [[\yii\mail\BaseMailer::EVENT_BEFORE_SEND]] may set this property to decide whether + * to continue send or not. + */ + public $isValid = true; } diff --git a/framework/base/Model.php b/framework/base/Model.php index 681db484196..8a4e93df114 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -55,886 +55,905 @@ */ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable { - use ArrayableTrait; - - /** - * The name of the default scenario. - */ - const SCENARIO_DEFAULT = 'default'; - /** - * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set - * [[ModelEvent::isValid]] to be false to stop the validation. - */ - const EVENT_BEFORE_VALIDATE = 'beforeValidate'; - /** - * @event Event an event raised at the end of [[validate()]] - */ - const EVENT_AFTER_VALIDATE = 'afterValidate'; - - /** - * @var array validation errors (attribute name => array of errors) - */ - private $_errors; - /** - * @var ArrayObject list of validators - */ - private $_validators; - /** - * @var string current scenario - */ - private $_scenario = self::SCENARIO_DEFAULT; - - /** - * Returns the validation rules for attributes. - * - * Validation rules are used by [[validate()]] to check if attribute values are valid. - * Child classes may override this method to declare different validation rules. - * - * Each rule is an array with the following structure: - * - * ~~~ - * [ - * ['attribute1', 'attribute2'], - * 'validator type', - * 'on' => ['scenario1', 'scenario2'], - * ...other parameters... - * ] - * ~~~ - * - * where - * - * - attribute list: required, specifies the attributes array to be validated, for single attribute you can pass string; - * - validator type: required, specifies the validator to be used. It can be a built-in validator name, - * a method name of the model class, an anonymous function, or a validator class name. - * - on: optional, specifies the [[scenario|scenarios]] array when the validation - * rule can be applied. If this option is not set, the rule will apply to all scenarios. - * - additional name-value pairs can be specified to initialize the corresponding validator properties. - * Please refer to individual validator class API for possible properties. - * - * A validator can be either an object of a class extending [[Validator]], or a model class method - * (called *inline validator*) that has the following signature: - * - * ~~~ - * // $params refers to validation parameters given in the rule - * function validatorName($attribute, $params) - * ~~~ - * - * In the above `$attribute` refers to currently validated attribute name while `$params` contains an array of - * validator configuration options such as `max` in case of `string` validator. Currently validate attribute value - * can be accessed as `$this->[$attribute]`. - * - * Yii also provides a set of [[Validator::builtInValidators|built-in validators]]. - * They each has an alias name which can be used when specifying a validation rule. - * - * Below are some examples: - * - * ~~~ - * [ - * // built-in "required" validator - * [['username', 'password'], 'required'], - * // built-in "string" validator customized with "min" and "max" properties - * ['username', 'string', 'min' => 3, 'max' => 12], - * // built-in "compare" validator that is used in "register" scenario only - * ['password', 'compare', 'compareAttribute' => 'password2', 'on' => 'register'], - * // an inline validator defined via the "authenticate()" method in the model class - * ['password', 'authenticate', 'on' => 'login'], - * // a validator of class "DateRangeValidator" - * ['dateRange', 'DateRangeValidator'], - * ]; - * ~~~ - * - * Note, in order to inherit rules defined in the parent class, a child class needs to - * merge the parent rules with child rules using functions such as `array_merge()`. - * - * @return array validation rules - * @see scenarios() - */ - public function rules() - { - return []; - } - - /** - * Returns a list of scenarios and the corresponding active attributes. - * An active attribute is one that is subject to validation in the current scenario. - * The returned array should be in the following format: - * - * ~~~ - * [ - * 'scenario1' => ['attribute11', 'attribute12', ...], - * 'scenario2' => ['attribute21', 'attribute22', ...], - * ... - * ] - * ~~~ - * - * By default, an active attribute is considered safe and can be massively assigned. - * If an attribute should NOT be massively assigned (thus considered unsafe), - * please prefix the attribute with an exclamation character (e.g. '!rank'). - * - * The default implementation of this method will return all scenarios found in the [[rules()]] - * declaration. A special scenario named [[SCENARIO_DEFAULT]] will contain all attributes - * found in the [[rules()]]. Each scenario will be associated with the attributes that - * are being validated by the validation rules that apply to the scenario. - * - * @return array a list of scenarios and the corresponding active attributes. - */ - public function scenarios() - { - $scenarios = [self::SCENARIO_DEFAULT => []]; - foreach ($this->getValidators() as $validator) { - foreach ($validator->on as $scenario) { - $scenarios[$scenario] = []; - } - foreach ($validator->except as $scenario) { - $scenarios[$scenario] = []; - } - } - $names = array_keys($scenarios); - - foreach ($this->getValidators() as $validator) { - if (empty($validator->on) && empty($validator->except)) { - foreach ($names as $name) { - foreach ($validator->attributes as $attribute) { - $scenarios[$name][$attribute] = true; - } - } - } elseif (empty($validator->on)) { - foreach ($names as $name) { - if (!in_array($name, $validator->except, true)) { - foreach ($validator->attributes as $attribute) { - $scenarios[$name][$attribute] = true; - } - } - } - } else { - foreach ($validator->on as $name) { - foreach ($validator->attributes as $attribute) { - $scenarios[$name][$attribute] = true; - } - } - } - } - - foreach ($scenarios as $scenario => $attributes) { - if (empty($attributes) && $scenario !== self::SCENARIO_DEFAULT) { - unset($scenarios[$scenario]); - } else { - $scenarios[$scenario] = array_keys($attributes); - } - } - - return $scenarios; - } - - /** - * Returns the form name that this model class should use. - * - * The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name - * the input fields for the attributes in a model. If the form name is "A" and an attribute - * name is "b", then the corresponding input name would be "A[b]". If the form name is - * an empty string, then the input name would be "b". - * - * By default, this method returns the model class name (without the namespace part) - * as the form name. You may override it when the model is used in different forms. - * - * @return string the form name of this model class. - */ - public function formName() - { - $reflector = new ReflectionClass($this); - return $reflector->getShortName(); - } - - /** - * Returns the list of attribute names. - * By default, this method returns all public non-static properties of the class. - * You may override this method to change the default behavior. - * @return array list of attribute names. - */ - public function attributes() - { - $class = new ReflectionClass($this); - $names = []; - foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { - if (!$property->isStatic()) { - $names[] = $property->getName(); - } - } - return $names; - } - - /** - * Returns the attribute labels. - * - * Attribute labels are mainly used for display purpose. For example, given an attribute - * `firstName`, we can declare a label `First Name` which is more user-friendly and can - * be displayed to end users. - * - * By default an attribute label is generated using [[generateAttributeLabel()]]. - * This method allows you to explicitly specify attribute labels. - * - * Note, in order to inherit labels defined in the parent class, a child class needs to - * merge the parent labels with child labels using functions such as `array_merge()`. - * - * @return array attribute labels (name => label) - * @see generateAttributeLabel() - */ - public function attributeLabels() - { - return []; - } - - /** - * Performs the data validation. - * - * This method executes the validation rules applicable to the current [[scenario]]. - * The following criteria are used to determine whether a rule is currently applicable: - * - * - the rule must be associated with the attributes relevant to the current scenario; - * - the rules must be effective for the current scenario. - * - * This method will call [[beforeValidate()]] and [[afterValidate()]] before and - * after the actual validation, respectively. If [[beforeValidate()]] returns false, - * the validation will be cancelled and [[afterValidate()]] will not be called. - * - * Errors found during the validation can be retrieved via [[getErrors()]], - * [[getFirstErrors()]] and [[getFirstError()]]. - * - * @param array $attributes list of attributes that should be validated. - * If this parameter is empty, it means any attribute listed in the applicable - * validation rules should be validated. - * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation - * @return boolean whether the validation is successful without any error. - * @throws InvalidParamException if the current scenario is unknown. - */ - public function validate($attributes = null, $clearErrors = true) - { - $scenarios = $this->scenarios(); - $scenario = $this->getScenario(); - if (!isset($scenarios[$scenario])) { - throw new InvalidParamException("Unknown scenario: $scenario"); - } - - if ($clearErrors) { - $this->clearErrors(); - } - if ($attributes === null) { - $attributes = $this->activeAttributes(); - } - if ($this->beforeValidate()) { - foreach ($this->getActiveValidators() as $validator) { - $validator->validateAttributes($this, $attributes); - } - $this->afterValidate(); - return !$this->hasErrors(); - } - return false; - } - - /** - * This method is invoked before validation starts. - * The default implementation raises a `beforeValidate` event. - * You may override this method to do preliminary checks before validation. - * Make sure the parent implementation is invoked so that the event can be raised. - * @return boolean whether the validation should be executed. Defaults to true. - * If false is returned, the validation will stop and the model is considered invalid. - */ - public function beforeValidate() - { - $event = new ModelEvent; - $this->trigger(self::EVENT_BEFORE_VALIDATE, $event); - return $event->isValid; - } - - /** - * This method is invoked after validation ends. - * The default implementation raises an `afterValidate` event. - * You may override this method to do postprocessing after validation. - * Make sure the parent implementation is invoked so that the event can be raised. - */ - public function afterValidate() - { - $this->trigger(self::EVENT_AFTER_VALIDATE); - } - - /** - * Returns all the validators declared in [[rules()]]. - * - * This method differs from [[getActiveValidators()]] in that the latter - * only returns the validators applicable to the current [[scenario]]. - * - * Because this method returns an ArrayObject object, you may - * manipulate it by inserting or removing validators (useful in model behaviors). - * For example, - * - * ~~~ - * $model->validators[] = $newValidator; - * ~~~ - * - * @return ArrayObject|\yii\validators\Validator[] all the validators declared in the model. - */ - public function getValidators() - { - if ($this->_validators === null) { - $this->_validators = $this->createValidators(); - } - return $this->_validators; - } - - /** - * Returns the validators applicable to the current [[scenario]]. - * @param string $attribute the name of the attribute whose applicable validators should be returned. - * If this is null, the validators for ALL attributes in the model will be returned. - * @return \yii\validators\Validator[] the validators applicable to the current [[scenario]]. - */ - public function getActiveValidators($attribute = null) - { - $validators = []; - $scenario = $this->getScenario(); - foreach ($this->getValidators() as $validator) { - if ($validator->isActive($scenario) && ($attribute === null || in_array($attribute, $validator->attributes, true))) { - $validators[] = $validator; - } - } - return $validators; - } - - /** - * Creates validator objects based on the validation rules specified in [[rules()]]. - * Unlike [[getValidators()]], each time this method is called, a new list of validators will be returned. - * @return ArrayObject validators - * @throws InvalidConfigException if any validation rule configuration is invalid - */ - public function createValidators() - { - $validators = new ArrayObject; - foreach ($this->rules() as $rule) { - if ($rule instanceof Validator) { - $validators->append($rule); - } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type - $validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2)); - $validators->append($validator); - } else { - throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); - } - } - return $validators; - } - - /** - * Returns a value indicating whether the attribute is required. - * This is determined by checking if the attribute is associated with a - * [[\yii\validators\RequiredValidator|required]] validation rule in the - * current [[scenario]]. - * @param string $attribute attribute name - * @return boolean whether the attribute is required - */ - public function isAttributeRequired($attribute) - { - foreach ($this->getActiveValidators($attribute) as $validator) { - if ($validator instanceof RequiredValidator) { - return true; - } - } - return false; - } - - /** - * Returns a value indicating whether the attribute is safe for massive assignments. - * @param string $attribute attribute name - * @return boolean whether the attribute is safe for massive assignments - * @see safeAttributes() - */ - public function isAttributeSafe($attribute) - { - return in_array($attribute, $this->safeAttributes(), true); - } - - /** - * Returns a value indicating whether the attribute is active in the current scenario. - * @param string $attribute attribute name - * @return boolean whether the attribute is active in the current scenario - * @see activeAttributes() - */ - public function isAttributeActive($attribute) - { - return in_array($attribute, $this->activeAttributes(), true); - } - - /** - * Returns the text label for the specified attribute. - * @param string $attribute the attribute name - * @return string the attribute label - * @see generateAttributeLabel() - * @see attributeLabels() - */ - public function getAttributeLabel($attribute) - { - $labels = $this->attributeLabels(); - return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute); - } - - /** - * Returns a value indicating whether there is any validation error. - * @param string|null $attribute attribute name. Use null to check all attributes. - * @return boolean whether there is any error. - */ - public function hasErrors($attribute = null) - { - return $attribute === null ? !empty($this->_errors) : isset($this->_errors[$attribute]); - } - - /** - * Returns the errors for all attribute or a single attribute. - * @param string $attribute attribute name. Use null to retrieve errors for all attributes. - * @property array An array of errors for all attributes. Empty array is returned if no error. - * The result is a two-dimensional array. See [[getErrors()]] for detailed description. - * @return array errors for all attributes or the specified attribute. Empty array is returned if no error. - * Note that when returning errors for all attributes, the result is a two-dimensional array, like the following: - * - * ~~~ - * [ - * 'username' => [ - * 'Username is required.', - * 'Username must contain only word characters.', - * ], - * 'email' => [ - * 'Email address is invalid.', - * ] - * ] - * ~~~ - * - * @see getFirstErrors() - * @see getFirstError() - */ - public function getErrors($attribute = null) - { - if ($attribute === null) { - return $this->_errors === null ? [] : $this->_errors; - } else { - return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : []; - } - } - - /** - * Returns the first error of every attribute in the model. - * @return array the first errors. The array keys are the attribute names, and the array - * values are the corresponding error messages. An empty array will be returned if there is no error. - * @see getErrors() - * @see getFirstError() - */ - public function getFirstErrors() - { - if (empty($this->_errors)) { - return []; - } else { - $errors = []; - foreach ($this->_errors as $name => $es) { - if (!empty($es)) { - $errors[$name] = reset($es); - } - } - return $errors; - } - } - - /** - * Returns the first error of the specified attribute. - * @param string $attribute attribute name. - * @return string the error message. Null is returned if no error. - * @see getErrors() - * @see getFirstErrors() - */ - public function getFirstError($attribute) - { - return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null; - } - - /** - * Adds a new error to the specified attribute. - * @param string $attribute attribute name - * @param string $error new error message - */ - public function addError($attribute, $error = '') - { - $this->_errors[$attribute][] = $error; - } - - /** - * Removes errors for all attributes or a single attribute. - * @param string $attribute attribute name. Use null to remove errors for all attribute. - */ - public function clearErrors($attribute = null) - { - if ($attribute === null) { - $this->_errors = []; - } else { - unset($this->_errors[$attribute]); - } - } - - /** - * Generates a user friendly attribute label based on the give attribute name. - * This is done by replacing underscores, dashes and dots with blanks and - * changing the first letter of each word to upper case. - * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'. - * @param string $name the column name - * @return string the attribute label - */ - public function generateAttributeLabel($name) - { - return Inflector::camel2words($name, true); - } - - /** - * Returns attribute values. - * @param array $names list of attributes whose value needs to be returned. - * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned. - * If it is an array, only the attributes in the array will be returned. - * @param array $except list of attributes whose value should NOT be returned. - * @return array attribute values (name => value). - */ - public function getAttributes($names = null, $except = []) - { - $values = []; - if ($names === null) { - $names = $this->attributes(); - } - foreach ($names as $name) { - $values[$name] = $this->$name; - } - foreach ($except as $name) { - unset($values[$name]); - } - - return $values; - } - - /** - * Sets the attribute values in a massive way. - * @param array $values attribute values (name => value) to be assigned to the model. - * @param boolean $safeOnly whether the assignments should only be done to the safe attributes. - * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. - * @see safeAttributes() - * @see attributes() - */ - public function setAttributes($values, $safeOnly = true) - { - if (is_array($values)) { - $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes()); - foreach ($values as $name => $value) { - if (isset($attributes[$name])) { - $this->$name = $value; - } elseif ($safeOnly) { - $this->onUnsafeAttribute($name, $value); - } - } - } - } - - /** - * This method is invoked when an unsafe attribute is being massively assigned. - * The default implementation will log a warning message if YII_DEBUG is on. - * It does nothing otherwise. - * @param string $name the unsafe attribute name - * @param mixed $value the attribute value - */ - public function onUnsafeAttribute($name, $value) - { - if (YII_DEBUG) { - Yii::trace("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); - } - } - - /** - * Returns the scenario that this model is used in. - * - * Scenario affects how validation is performed and which attributes can - * be massively assigned. - * - * @return string the scenario that this model is in. Defaults to [[SCENARIO_DEFAULT]]. - */ - public function getScenario() - { - return $this->_scenario; - } - - /** - * Sets the scenario for the model. - * Note that this method does not check if the scenario exists or not. - * The method [[validate()]] will perform this check. - * @param string $value the scenario that this model is in. - */ - public function setScenario($value) - { - $this->_scenario = $value; - } - - /** - * Returns the attribute names that are safe to be massively assigned in the current scenario. - * @return string[] safe attribute names - */ - public function safeAttributes() - { - $scenario = $this->getScenario(); - $scenarios = $this->scenarios(); - if (!isset($scenarios[$scenario])) { - return []; - } - $attributes = []; - foreach ($scenarios[$scenario] as $attribute) { - if ($attribute[0] !== '!') { - $attributes[] = $attribute; - } - } - return $attributes; - } - - /** - * Returns the attribute names that are subject to validation in the current scenario. - * @return string[] safe attribute names - */ - public function activeAttributes() - { - $scenario = $this->getScenario(); - $scenarios = $this->scenarios(); - if (!isset($scenarios[$scenario])) { - return []; - } - $attributes = $scenarios[$scenario]; - foreach ($attributes as $i => $attribute) { - if ($attribute[0] === '!') { - $attributes[$i] = substr($attribute, 1); - } - } - return $attributes; - } - - /** - * Populates the model with the data from end user. - * The data to be loaded is `$data[formName]`, where `formName` refers to the value of [[formName()]]. - * If [[formName()]] is empty, the whole `$data` array will be used to populate the model. - * The data being populated is subject to the safety check by [[setAttributes()]]. - * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array - * supplied by end user. - * @param string $formName the form name to be used for loading the data into the model. - * If not set, [[formName()]] will be used. - * @return boolean whether the model is successfully populated with some data. - */ - public function load($data, $formName = null) - { - $scope = $formName === null ? $this->formName() : $formName; - if ($scope == '' && !empty($data)) { - $this->setAttributes($data); - return true; - } elseif (isset($data[$scope])) { - $this->setAttributes($data[$scope]); - return true; - } else { - return false; - } - } - - /** - * Populates a set of models with the data from end user. - * This method is mainly used to collect tabular data input. - * The data to be loaded for each model is `$data[formName][index]`, where `formName` - * refers to the value of [[formName()]], and `index` the index of the model in the `$models` array. - * If [[formName()]] is empty, `$data[index]` will be used to populate each model. - * The data being populated to each model is subject to the safety check by [[setAttributes()]]. - * @param array $models the models to be populated. Note that all models should have the same class. - * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array - * supplied by end user. - * @return boolean whether the model is successfully populated with some data. - */ - public static function loadMultiple($models, $data) - { - /** @var Model $model */ - $model = reset($models); - if ($model === false) { - return false; - } - $success = false; - $scope = $model->formName(); - foreach ($models as $i => $model) { - if ($scope == '') { - if (isset($data[$i])) { - $model->setAttributes($data[$i]); - $success = true; - } - } elseif (isset($data[$scope][$i])) { - $model->setAttributes($data[$scope][$i]); - $success = true; - } - } - return $success; - } - - /** - * Validates multiple models. - * This method will validate every model. The models being validated may - * be of the same or different types. - * @param array $models the models to be validated - * @param array $attributes list of attributes that should be validated. - * If this parameter is empty, it means any attribute listed in the applicable - * validation rules should be validated. - * @return boolean whether all models are valid. False will be returned if one - * or multiple models have validation error. - */ - public static function validateMultiple($models, $attributes = null) - { - $valid = true; - /** @var Model $model */ - foreach ($models as $model) { - $valid = $model->validate($attributes) && $valid; - } - return $valid; - } - - /** - * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. - * - * A field is a named element in the returned array by [[toArray()]]. - * - * This method should return an array of field names or field definitions. - * If the former, the field name will be treated as an object property name whose value will be used - * as the field value. If the latter, the array key should be the field name while the array value should be - * the corresponding field definition which can be either an object property name or a PHP callable - * returning the corresponding field value. The signature of the callable should be: - * - * ```php - * function ($field, $model) { - * // return field value - * } - * ``` - * - * For example, the following code declares four fields: - * - * - `email`: the field name is the same as the property name `email`; - * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their - * values are obtained from the `first_name` and `last_name` properties; - * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` - * and `last_name`. - * - * ```php - * return [ - * 'email', - * 'firstName' => 'first_name', - * 'lastName' => 'last_name', - * 'fullName' => function () { - * return $this->first_name . ' ' . $this->last_name; - * }, - * ]; - * ``` - * - * In this method, you may also want to return different lists of fields based on some context - * information. For example, depending on [[scenario]] or the privilege of the current application user, - * you may return different sets of visible fields or filter out some fields. - * - * The default implementation of this method returns [[attributes()]] indexed by the same attribute names. - * - * @return array the list of field names or field definitions. - * @see toArray() - */ - public function fields() - { - $fields = $this->attributes(); - return array_combine($fields, $fields); - } - - /** - * Determines which fields can be returned by [[toArray()]]. - * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]] - * to determine which fields can be returned. - * @param array $fields the fields being requested for exporting - * @param array $expand the additional fields being requested for exporting - * @return array the list of fields to be exported. The array keys are the field names, and the array values - * are the corresponding object property names or PHP callables returning the field values. - */ - protected function resolveFields(array $fields, array $expand) - { - $result = []; - - foreach ($this->fields() as $field => $definition) { - if (is_integer($field)) { - $field = $definition; - } - if (empty($fields) || in_array($field, $fields, true)) { - $result[$field] = $definition; - } - } - - if (empty($expand)) { - return $result; - } - - foreach ($this->extraFields() as $field => $definition) { - if (is_integer($field)) { - $field = $definition; - } - if (in_array($field, $expand, true)) { - $result[$field] = $definition; - } - } - - return $result; - } - - /** - * Returns an iterator for traversing the attributes in the model. - * This method is required by the interface IteratorAggregate. - * @return ArrayIterator an iterator for traversing the items in the list. - */ - public function getIterator() - { - $attributes = $this->getAttributes(); - return new ArrayIterator($attributes); - } - - /** - * Returns whether there is an element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($model[$offset])`. - * @param mixed $offset the offset to check on - * @return boolean - */ - public function offsetExists($offset) - { - return $this->$offset !== null; - } - - /** - * Returns the element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$value = $model[$offset];`. - * @param mixed $offset the offset to retrieve element. - * @return mixed the element at the offset, null if no element is found at the offset - */ - public function offsetGet($offset) - { - return $this->$offset; - } - - /** - * Sets the element at the specified offset. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$model[$offset] = $item;`. - * @param integer $offset the offset to set element - * @param mixed $item the element value - */ - public function offsetSet($offset, $item) - { - $this->$offset = $item; - } - - /** - * Sets the element value at the specified offset to null. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($model[$offset])`. - * @param mixed $offset the offset to unset element - */ - public function offsetUnset($offset) - { - $this->$offset = null; - } + use ArrayableTrait; + + /** + * The name of the default scenario. + */ + const SCENARIO_DEFAULT = 'default'; + /** + * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set + * [[ModelEvent::isValid]] to be false to stop the validation. + */ + const EVENT_BEFORE_VALIDATE = 'beforeValidate'; + /** + * @event Event an event raised at the end of [[validate()]] + */ + const EVENT_AFTER_VALIDATE = 'afterValidate'; + + /** + * @var array validation errors (attribute name => array of errors) + */ + private $_errors; + /** + * @var ArrayObject list of validators + */ + private $_validators; + /** + * @var string current scenario + */ + private $_scenario = self::SCENARIO_DEFAULT; + + /** + * Returns the validation rules for attributes. + * + * Validation rules are used by [[validate()]] to check if attribute values are valid. + * Child classes may override this method to declare different validation rules. + * + * Each rule is an array with the following structure: + * + * ~~~ + * [ + * ['attribute1', 'attribute2'], + * 'validator type', + * 'on' => ['scenario1', 'scenario2'], + * ...other parameters... + * ] + * ~~~ + * + * where + * + * - attribute list: required, specifies the attributes array to be validated, for single attribute you can pass string; + * - validator type: required, specifies the validator to be used. It can be a built-in validator name, + * a method name of the model class, an anonymous function, or a validator class name. + * - on: optional, specifies the [[scenario|scenarios]] array when the validation + * rule can be applied. If this option is not set, the rule will apply to all scenarios. + * - additional name-value pairs can be specified to initialize the corresponding validator properties. + * Please refer to individual validator class API for possible properties. + * + * A validator can be either an object of a class extending [[Validator]], or a model class method + * (called *inline validator*) that has the following signature: + * + * ~~~ + * // $params refers to validation parameters given in the rule + * function validatorName($attribute, $params) + * ~~~ + * + * In the above `$attribute` refers to currently validated attribute name while `$params` contains an array of + * validator configuration options such as `max` in case of `string` validator. Currently validate attribute value + * can be accessed as `$this->[$attribute]`. + * + * Yii also provides a set of [[Validator::builtInValidators|built-in validators]]. + * They each has an alias name which can be used when specifying a validation rule. + * + * Below are some examples: + * + * ~~~ + * [ + * // built-in "required" validator + * [['username', 'password'], 'required'], + * // built-in "string" validator customized with "min" and "max" properties + * ['username', 'string', 'min' => 3, 'max' => 12], + * // built-in "compare" validator that is used in "register" scenario only + * ['password', 'compare', 'compareAttribute' => 'password2', 'on' => 'register'], + * // an inline validator defined via the "authenticate()" method in the model class + * ['password', 'authenticate', 'on' => 'login'], + * // a validator of class "DateRangeValidator" + * ['dateRange', 'DateRangeValidator'], + * ]; + * ~~~ + * + * Note, in order to inherit rules defined in the parent class, a child class needs to + * merge the parent rules with child rules using functions such as `array_merge()`. + * + * @return array validation rules + * @see scenarios() + */ + public function rules() + { + return []; + } + + /** + * Returns a list of scenarios and the corresponding active attributes. + * An active attribute is one that is subject to validation in the current scenario. + * The returned array should be in the following format: + * + * ~~~ + * [ + * 'scenario1' => ['attribute11', 'attribute12', ...], + * 'scenario2' => ['attribute21', 'attribute22', ...], + * ... + * ] + * ~~~ + * + * By default, an active attribute is considered safe and can be massively assigned. + * If an attribute should NOT be massively assigned (thus considered unsafe), + * please prefix the attribute with an exclamation character (e.g. '!rank'). + * + * The default implementation of this method will return all scenarios found in the [[rules()]] + * declaration. A special scenario named [[SCENARIO_DEFAULT]] will contain all attributes + * found in the [[rules()]]. Each scenario will be associated with the attributes that + * are being validated by the validation rules that apply to the scenario. + * + * @return array a list of scenarios and the corresponding active attributes. + */ + public function scenarios() + { + $scenarios = [self::SCENARIO_DEFAULT => []]; + foreach ($this->getValidators() as $validator) { + foreach ($validator->on as $scenario) { + $scenarios[$scenario] = []; + } + foreach ($validator->except as $scenario) { + $scenarios[$scenario] = []; + } + } + $names = array_keys($scenarios); + + foreach ($this->getValidators() as $validator) { + if (empty($validator->on) && empty($validator->except)) { + foreach ($names as $name) { + foreach ($validator->attributes as $attribute) { + $scenarios[$name][$attribute] = true; + } + } + } elseif (empty($validator->on)) { + foreach ($names as $name) { + if (!in_array($name, $validator->except, true)) { + foreach ($validator->attributes as $attribute) { + $scenarios[$name][$attribute] = true; + } + } + } + } else { + foreach ($validator->on as $name) { + foreach ($validator->attributes as $attribute) { + $scenarios[$name][$attribute] = true; + } + } + } + } + + foreach ($scenarios as $scenario => $attributes) { + if (empty($attributes) && $scenario !== self::SCENARIO_DEFAULT) { + unset($scenarios[$scenario]); + } else { + $scenarios[$scenario] = array_keys($attributes); + } + } + + return $scenarios; + } + + /** + * Returns the form name that this model class should use. + * + * The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name + * the input fields for the attributes in a model. If the form name is "A" and an attribute + * name is "b", then the corresponding input name would be "A[b]". If the form name is + * an empty string, then the input name would be "b". + * + * By default, this method returns the model class name (without the namespace part) + * as the form name. You may override it when the model is used in different forms. + * + * @return string the form name of this model class. + */ + public function formName() + { + $reflector = new ReflectionClass($this); + + return $reflector->getShortName(); + } + + /** + * Returns the list of attribute names. + * By default, this method returns all public non-static properties of the class. + * You may override this method to change the default behavior. + * @return array list of attribute names. + */ + public function attributes() + { + $class = new ReflectionClass($this); + $names = []; + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + if (!$property->isStatic()) { + $names[] = $property->getName(); + } + } + + return $names; + } + + /** + * Returns the attribute labels. + * + * Attribute labels are mainly used for display purpose. For example, given an attribute + * `firstName`, we can declare a label `First Name` which is more user-friendly and can + * be displayed to end users. + * + * By default an attribute label is generated using [[generateAttributeLabel()]]. + * This method allows you to explicitly specify attribute labels. + * + * Note, in order to inherit labels defined in the parent class, a child class needs to + * merge the parent labels with child labels using functions such as `array_merge()`. + * + * @return array attribute labels (name => label) + * @see generateAttributeLabel() + */ + public function attributeLabels() + { + return []; + } + + /** + * Performs the data validation. + * + * This method executes the validation rules applicable to the current [[scenario]]. + * The following criteria are used to determine whether a rule is currently applicable: + * + * - the rule must be associated with the attributes relevant to the current scenario; + * - the rules must be effective for the current scenario. + * + * This method will call [[beforeValidate()]] and [[afterValidate()]] before and + * after the actual validation, respectively. If [[beforeValidate()]] returns false, + * the validation will be cancelled and [[afterValidate()]] will not be called. + * + * Errors found during the validation can be retrieved via [[getErrors()]], + * [[getFirstErrors()]] and [[getFirstError()]]. + * + * @param array $attributes list of attributes that should be validated. + * If this parameter is empty, it means any attribute listed in the applicable + * validation rules should be validated. + * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation + * @return boolean whether the validation is successful without any error. + * @throws InvalidParamException if the current scenario is unknown. + */ + public function validate($attributes = null, $clearErrors = true) + { + $scenarios = $this->scenarios(); + $scenario = $this->getScenario(); + if (!isset($scenarios[$scenario])) { + throw new InvalidParamException("Unknown scenario: $scenario"); + } + + if ($clearErrors) { + $this->clearErrors(); + } + if ($attributes === null) { + $attributes = $this->activeAttributes(); + } + if ($this->beforeValidate()) { + foreach ($this->getActiveValidators() as $validator) { + $validator->validateAttributes($this, $attributes); + } + $this->afterValidate(); + + return !$this->hasErrors(); + } + + return false; + } + + /** + * This method is invoked before validation starts. + * The default implementation raises a `beforeValidate` event. + * You may override this method to do preliminary checks before validation. + * Make sure the parent implementation is invoked so that the event can be raised. + * @return boolean whether the validation should be executed. Defaults to true. + * If false is returned, the validation will stop and the model is considered invalid. + */ + public function beforeValidate() + { + $event = new ModelEvent; + $this->trigger(self::EVENT_BEFORE_VALIDATE, $event); + + return $event->isValid; + } + + /** + * This method is invoked after validation ends. + * The default implementation raises an `afterValidate` event. + * You may override this method to do postprocessing after validation. + * Make sure the parent implementation is invoked so that the event can be raised. + */ + public function afterValidate() + { + $this->trigger(self::EVENT_AFTER_VALIDATE); + } + + /** + * Returns all the validators declared in [[rules()]]. + * + * This method differs from [[getActiveValidators()]] in that the latter + * only returns the validators applicable to the current [[scenario]]. + * + * Because this method returns an ArrayObject object, you may + * manipulate it by inserting or removing validators (useful in model behaviors). + * For example, + * + * ~~~ + * $model->validators[] = $newValidator; + * ~~~ + * + * @return ArrayObject|\yii\validators\Validator[] all the validators declared in the model. + */ + public function getValidators() + { + if ($this->_validators === null) { + $this->_validators = $this->createValidators(); + } + + return $this->_validators; + } + + /** + * Returns the validators applicable to the current [[scenario]]. + * @param string $attribute the name of the attribute whose applicable validators should be returned. + * If this is null, the validators for ALL attributes in the model will be returned. + * @return \yii\validators\Validator[] the validators applicable to the current [[scenario]]. + */ + public function getActiveValidators($attribute = null) + { + $validators = []; + $scenario = $this->getScenario(); + foreach ($this->getValidators() as $validator) { + if ($validator->isActive($scenario) && ($attribute === null || in_array($attribute, $validator->attributes, true))) { + $validators[] = $validator; + } + } + + return $validators; + } + + /** + * Creates validator objects based on the validation rules specified in [[rules()]]. + * Unlike [[getValidators()]], each time this method is called, a new list of validators will be returned. + * @return ArrayObject validators + * @throws InvalidConfigException if any validation rule configuration is invalid + */ + public function createValidators() + { + $validators = new ArrayObject; + foreach ($this->rules() as $rule) { + if ($rule instanceof Validator) { + $validators->append($rule); + } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type + $validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2)); + $validators->append($validator); + } else { + throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); + } + } + + return $validators; + } + + /** + * Returns a value indicating whether the attribute is required. + * This is determined by checking if the attribute is associated with a + * [[\yii\validators\RequiredValidator|required]] validation rule in the + * current [[scenario]]. + * @param string $attribute attribute name + * @return boolean whether the attribute is required + */ + public function isAttributeRequired($attribute) + { + foreach ($this->getActiveValidators($attribute) as $validator) { + if ($validator instanceof RequiredValidator) { + return true; + } + } + + return false; + } + + /** + * Returns a value indicating whether the attribute is safe for massive assignments. + * @param string $attribute attribute name + * @return boolean whether the attribute is safe for massive assignments + * @see safeAttributes() + */ + public function isAttributeSafe($attribute) + { + return in_array($attribute, $this->safeAttributes(), true); + } + + /** + * Returns a value indicating whether the attribute is active in the current scenario. + * @param string $attribute attribute name + * @return boolean whether the attribute is active in the current scenario + * @see activeAttributes() + */ + public function isAttributeActive($attribute) + { + return in_array($attribute, $this->activeAttributes(), true); + } + + /** + * Returns the text label for the specified attribute. + * @param string $attribute the attribute name + * @return string the attribute label + * @see generateAttributeLabel() + * @see attributeLabels() + */ + public function getAttributeLabel($attribute) + { + $labels = $this->attributeLabels(); + + return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute); + } + + /** + * Returns a value indicating whether there is any validation error. + * @param string|null $attribute attribute name. Use null to check all attributes. + * @return boolean whether there is any error. + */ + public function hasErrors($attribute = null) + { + return $attribute === null ? !empty($this->_errors) : isset($this->_errors[$attribute]); + } + + /** + * Returns the errors for all attribute or a single attribute. + * @param string $attribute attribute name. Use null to retrieve errors for all attributes. + * @property array An array of errors for all attributes. Empty array is returned if no error. + * The result is a two-dimensional array. See [[getErrors()]] for detailed description. + * @return array errors for all attributes or the specified attribute. Empty array is returned if no error. + * Note that when returning errors for all attributes, the result is a two-dimensional array, like the following: + * + * ~~~ + * [ + * 'username' => [ + * 'Username is required.', + * 'Username must contain only word characters.', + * ], + * 'email' => [ + * 'Email address is invalid.', + * ] + * ] + * ~~~ + * + * @see getFirstErrors() + * @see getFirstError() + */ + public function getErrors($attribute = null) + { + if ($attribute === null) { + return $this->_errors === null ? [] : $this->_errors; + } else { + return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : []; + } + } + + /** + * Returns the first error of every attribute in the model. + * @return array the first errors. The array keys are the attribute names, and the array + * values are the corresponding error messages. An empty array will be returned if there is no error. + * @see getErrors() + * @see getFirstError() + */ + public function getFirstErrors() + { + if (empty($this->_errors)) { + return []; + } else { + $errors = []; + foreach ($this->_errors as $name => $es) { + if (!empty($es)) { + $errors[$name] = reset($es); + } + } + + return $errors; + } + } + + /** + * Returns the first error of the specified attribute. + * @param string $attribute attribute name. + * @return string the error message. Null is returned if no error. + * @see getErrors() + * @see getFirstErrors() + */ + public function getFirstError($attribute) + { + return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null; + } + + /** + * Adds a new error to the specified attribute. + * @param string $attribute attribute name + * @param string $error new error message + */ + public function addError($attribute, $error = '') + { + $this->_errors[$attribute][] = $error; + } + + /** + * Removes errors for all attributes or a single attribute. + * @param string $attribute attribute name. Use null to remove errors for all attribute. + */ + public function clearErrors($attribute = null) + { + if ($attribute === null) { + $this->_errors = []; + } else { + unset($this->_errors[$attribute]); + } + } + + /** + * Generates a user friendly attribute label based on the give attribute name. + * This is done by replacing underscores, dashes and dots with blanks and + * changing the first letter of each word to upper case. + * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'. + * @param string $name the column name + * @return string the attribute label + */ + public function generateAttributeLabel($name) + { + return Inflector::camel2words($name, true); + } + + /** + * Returns attribute values. + * @param array $names list of attributes whose value needs to be returned. + * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned. + * If it is an array, only the attributes in the array will be returned. + * @param array $except list of attributes whose value should NOT be returned. + * @return array attribute values (name => value). + */ + public function getAttributes($names = null, $except = []) + { + $values = []; + if ($names === null) { + $names = $this->attributes(); + } + foreach ($names as $name) { + $values[$name] = $this->$name; + } + foreach ($except as $name) { + unset($values[$name]); + } + + return $values; + } + + /** + * Sets the attribute values in a massive way. + * @param array $values attribute values (name => value) to be assigned to the model. + * @param boolean $safeOnly whether the assignments should only be done to the safe attributes. + * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. + * @see safeAttributes() + * @see attributes() + */ + public function setAttributes($values, $safeOnly = true) + { + if (is_array($values)) { + $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes()); + foreach ($values as $name => $value) { + if (isset($attributes[$name])) { + $this->$name = $value; + } elseif ($safeOnly) { + $this->onUnsafeAttribute($name, $value); + } + } + } + } + + /** + * This method is invoked when an unsafe attribute is being massively assigned. + * The default implementation will log a warning message if YII_DEBUG is on. + * It does nothing otherwise. + * @param string $name the unsafe attribute name + * @param mixed $value the attribute value + */ + public function onUnsafeAttribute($name, $value) + { + if (YII_DEBUG) { + Yii::trace("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); + } + } + + /** + * Returns the scenario that this model is used in. + * + * Scenario affects how validation is performed and which attributes can + * be massively assigned. + * + * @return string the scenario that this model is in. Defaults to [[SCENARIO_DEFAULT]]. + */ + public function getScenario() + { + return $this->_scenario; + } + + /** + * Sets the scenario for the model. + * Note that this method does not check if the scenario exists or not. + * The method [[validate()]] will perform this check. + * @param string $value the scenario that this model is in. + */ + public function setScenario($value) + { + $this->_scenario = $value; + } + + /** + * Returns the attribute names that are safe to be massively assigned in the current scenario. + * @return string[] safe attribute names + */ + public function safeAttributes() + { + $scenario = $this->getScenario(); + $scenarios = $this->scenarios(); + if (!isset($scenarios[$scenario])) { + return []; + } + $attributes = []; + foreach ($scenarios[$scenario] as $attribute) { + if ($attribute[0] !== '!') { + $attributes[] = $attribute; + } + } + + return $attributes; + } + + /** + * Returns the attribute names that are subject to validation in the current scenario. + * @return string[] safe attribute names + */ + public function activeAttributes() + { + $scenario = $this->getScenario(); + $scenarios = $this->scenarios(); + if (!isset($scenarios[$scenario])) { + return []; + } + $attributes = $scenarios[$scenario]; + foreach ($attributes as $i => $attribute) { + if ($attribute[0] === '!') { + $attributes[$i] = substr($attribute, 1); + } + } + + return $attributes; + } + + /** + * Populates the model with the data from end user. + * The data to be loaded is `$data[formName]`, where `formName` refers to the value of [[formName()]]. + * If [[formName()]] is empty, the whole `$data` array will be used to populate the model. + * The data being populated is subject to the safety check by [[setAttributes()]]. + * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array + * supplied by end user. + * @param string $formName the form name to be used for loading the data into the model. + * If not set, [[formName()]] will be used. + * @return boolean whether the model is successfully populated with some data. + */ + public function load($data, $formName = null) + { + $scope = $formName === null ? $this->formName() : $formName; + if ($scope == '' && !empty($data)) { + $this->setAttributes($data); + + return true; + } elseif (isset($data[$scope])) { + $this->setAttributes($data[$scope]); + + return true; + } else { + return false; + } + } + + /** + * Populates a set of models with the data from end user. + * This method is mainly used to collect tabular data input. + * The data to be loaded for each model is `$data[formName][index]`, where `formName` + * refers to the value of [[formName()]], and `index` the index of the model in the `$models` array. + * If [[formName()]] is empty, `$data[index]` will be used to populate each model. + * The data being populated to each model is subject to the safety check by [[setAttributes()]]. + * @param array $models the models to be populated. Note that all models should have the same class. + * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array + * supplied by end user. + * @return boolean whether the model is successfully populated with some data. + */ + public static function loadMultiple($models, $data) + { + /** @var Model $model */ + $model = reset($models); + if ($model === false) { + return false; + } + $success = false; + $scope = $model->formName(); + foreach ($models as $i => $model) { + if ($scope == '') { + if (isset($data[$i])) { + $model->setAttributes($data[$i]); + $success = true; + } + } elseif (isset($data[$scope][$i])) { + $model->setAttributes($data[$scope][$i]); + $success = true; + } + } + + return $success; + } + + /** + * Validates multiple models. + * This method will validate every model. The models being validated may + * be of the same or different types. + * @param array $models the models to be validated + * @param array $attributes list of attributes that should be validated. + * If this parameter is empty, it means any attribute listed in the applicable + * validation rules should be validated. + * @return boolean whether all models are valid. False will be returned if one + * or multiple models have validation error. + */ + public static function validateMultiple($models, $attributes = null) + { + $valid = true; + /** @var Model $model */ + foreach ($models as $model) { + $valid = $model->validate($attributes) && $valid; + } + + return $valid; + } + + /** + * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified. + * + * A field is a named element in the returned array by [[toArray()]]. + * + * This method should return an array of field names or field definitions. + * If the former, the field name will be treated as an object property name whose value will be used + * as the field value. If the latter, the array key should be the field name while the array value should be + * the corresponding field definition which can be either an object property name or a PHP callable + * returning the corresponding field value. The signature of the callable should be: + * + * ```php + * function ($field, $model) { + * // return field value + * } + * ``` + * + * For example, the following code declares four fields: + * + * - `email`: the field name is the same as the property name `email`; + * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their + * values are obtained from the `first_name` and `last_name` properties; + * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` + * and `last_name`. + * + * ```php + * return [ + * 'email', + * 'firstName' => 'first_name', + * 'lastName' => 'last_name', + * 'fullName' => function () { + * return $this->first_name . ' ' . $this->last_name; + * }, + * ]; + * ``` + * + * In this method, you may also want to return different lists of fields based on some context + * information. For example, depending on [[scenario]] or the privilege of the current application user, + * you may return different sets of visible fields or filter out some fields. + * + * The default implementation of this method returns [[attributes()]] indexed by the same attribute names. + * + * @return array the list of field names or field definitions. + * @see toArray() + */ + public function fields() + { + $fields = $this->attributes(); + + return array_combine($fields, $fields); + } + + /** + * Determines which fields can be returned by [[toArray()]]. + * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]] + * to determine which fields can be returned. + * @param array $fields the fields being requested for exporting + * @param array $expand the additional fields being requested for exporting + * @return array the list of fields to be exported. The array keys are the field names, and the array values + * are the corresponding object property names or PHP callables returning the field values. + */ + protected function resolveFields(array $fields, array $expand) + { + $result = []; + + foreach ($this->fields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (empty($fields) || in_array($field, $fields, true)) { + $result[$field] = $definition; + } + } + + if (empty($expand)) { + return $result; + } + + foreach ($this->extraFields() as $field => $definition) { + if (is_integer($field)) { + $field = $definition; + } + if (in_array($field, $expand, true)) { + $result[$field] = $definition; + } + } + + return $result; + } + + /** + * Returns an iterator for traversing the attributes in the model. + * This method is required by the interface IteratorAggregate. + * @return ArrayIterator an iterator for traversing the items in the list. + */ + public function getIterator() + { + $attributes = $this->getAttributes(); + + return new ArrayIterator($attributes); + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `isset($model[$offset])`. + * @param mixed $offset the offset to check on + * @return boolean + */ + public function offsetExists($offset) + { + return $this->$offset !== null; + } + + /** + * Returns the element at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$value = $model[$offset];`. + * @param mixed $offset the offset to retrieve element. + * @return mixed the element at the offset, null if no element is found at the offset + */ + public function offsetGet($offset) + { + return $this->$offset; + } + + /** + * Sets the element at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$model[$offset] = $item;`. + * @param integer $offset the offset to set element + * @param mixed $item the element value + */ + public function offsetSet($offset, $item) + { + $this->$offset = $item; + } + + /** + * Sets the element value at the specified offset to null. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($model[$offset])`. + * @param mixed $offset the offset to unset element + */ + public function offsetUnset($offset) + { + $this->$offset = null; + } } diff --git a/framework/base/ModelEvent.php b/framework/base/ModelEvent.php index 57e41f9ce30..13e03ea971b 100644 --- a/framework/base/ModelEvent.php +++ b/framework/base/ModelEvent.php @@ -17,9 +17,9 @@ */ class ModelEvent extends Event { - /** - * @var boolean whether the model is in valid status. Defaults to true. - * A model is in valid status if it passes validations or certain checks. - */ - public $isValid = true; + /** + * @var boolean whether the model is in valid status. Defaults to true. + * A model is in valid status if it passes validations or certain checks. + */ + public $isValid = true; } diff --git a/framework/base/Module.php b/framework/base/Module.php index eb7ce90a052..ef7ac0846fe 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -37,679 +37,689 @@ */ class Module extends Component { - /** - * @var array custom module parameters (name => value). - */ - public $params = []; - /** - * @var array the IDs of the components or modules that should be preloaded right after initialization. - */ - public $preload = []; - /** - * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. - */ - public $id; - /** - * @var Module the parent module of this module. Null if this module does not have a parent. - */ - public $module; - /** - * @var string|boolean the layout that should be applied for views within this module. This refers to a view name - * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] - * will be taken. If this is false, layout will be disabled within this module. - */ - public $layout; - /** - * @var array mapping from controller ID to controller configurations. - * Each name-value pair specifies the configuration of a single controller. - * A controller configuration can be either a string or an array. - * If the former, the string should be the fully qualified class name of the controller. - * If the latter, the array must contain a 'class' element which specifies - * the controller's fully qualified class name, and the rest of the name-value pairs - * in the array are used to initialize the corresponding controller properties. For example, - * - * ~~~ - * [ - * 'account' => 'app\controllers\UserController', - * 'article' => [ - * 'class' => 'app\controllers\PostController', - * 'pageTitle' => 'something new', - * ], - * ] - * ~~~ - */ - public $controllerMap = []; - /** - * @var string the namespace that controller classes are in. If not set, - * it will use the "controllers" sub-namespace under the namespace of this module. - * For example, if the namespace of this module is "foo\bar", then the default - * controller namespace would be "foo\bar\controllers". - */ - public $controllerNamespace; - /** - * @return string the default route of this module. Defaults to 'default'. - * The route may consist of child module ID, controller ID, and/or action ID. - * For example, `help`, `post/create`, `admin/post/create`. - * If action ID is not given, it will take the default value as specified in - * [[Controller::defaultAction]]. - */ - public $defaultRoute = 'default'; - /** - * @var string the root directory of the module. - */ - private $_basePath; - /** - * @var string the root directory that contains view files for this module - */ - private $_viewPath; - /** - * @var string the root directory that contains layout view files for this module. - */ - private $_layoutPath; - /** - * @var array child modules of this module - */ - private $_modules = []; - /** - * @var array components registered under this module - */ - private $_components = []; - - /** - * Constructor. - * @param string $id the ID of this module - * @param Module $parent the parent module (if any) - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $parent = null, $config = []) - { - $this->id = $id; - $this->module = $parent; - parent::__construct($config); - } - - /** - * Getter magic method. - * This method is overridden to support accessing components - * like reading module properties. - * @param string $name component or property name - * @return mixed the named property value - */ - public function __get($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name); - } else { - return parent::__get($name); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking - * if the named component is loaded. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name) !== null; - } else { - return parent::__isset($name); - } - } - - /** - * Initializes the module. - * This method is called after the module is created and initialized with property values - * given in configuration. The default implementation will call [[preloadComponents()]] to - * load components that are declared in [[preload]]. - * - * If you override this method, please make sure you call the parent implementation. - */ - public function init() - { - if ($this->controllerNamespace === null) { - $class = get_class($this); - if (($pos = strrpos($class, '\\')) !== false) { - $this->controllerNamespace = substr($class, 0, $pos) . '\\controllers'; - } - } - $this->preloadComponents(); - } - - /** - * Returns an ID that uniquely identifies this module among all modules within the current application. - * Note that if the module is an application, an empty string will be returned. - * @return string the unique ID of the module. - */ - public function getUniqueId() - { - return $this->module ? ltrim($this->module->getUniqueId() . '/' . $this->id, '/') : $this->id; - } - - /** - * Returns the root directory of the module. - * It defaults to the directory containing the module class file. - * @return string the root directory of the module. - */ - public function getBasePath() - { - if ($this->_basePath === null) { - $class = new \ReflectionClass($this); - $this->_basePath = dirname($class->getFileName()); - } - return $this->_basePath; - } - - /** - * Sets the root directory of the module. - * This method can only be invoked at the beginning of the constructor. - * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws InvalidParamException if the directory does not exist. - */ - public function setBasePath($path) - { - $path = Yii::getAlias($path); - $p = realpath($path); - if ($p !== false && is_dir($p)) { - $this->_basePath = $p; - } else { - throw new InvalidParamException("The directory does not exist: $path"); - } - } - - /** - * Returns the directory that contains the controller classes according to [[controllerNamespace]]. - * Note that in order for this method to return a value, you must define - * an alias for the root namespace of [[controllerNamespace]]. - * @return string the directory that contains the controller classes. - * @throws InvalidParamException if there is no alias defined for the root namespace of [[controllerNamespace]]. - */ - public function getControllerPath() - { - return Yii::getAlias('@' . str_replace('\\', '/', $this->controllerNamespace)); - } - - /** - * Returns the directory that contains the view files for this module. - * @return string the root directory of view files. Defaults to "[[basePath]]/view". - */ - public function getViewPath() - { - if ($this->_viewPath !== null) { - return $this->_viewPath; - } else { - return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; - } - } - - /** - * Sets the directory that contains the view files. - * @param string $path the root directory of view files. - * @throws InvalidParamException if the directory is invalid - */ - public function setViewPath($path) - { - $this->_viewPath = Yii::getAlias($path); - } - - /** - * Returns the directory that contains layout view files for this module. - * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". - */ - public function getLayoutPath() - { - if ($this->_layoutPath !== null) { - return $this->_layoutPath; - } else { - return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; - } - } - - /** - * Sets the directory that contains the layout files. - * @param string $path the root directory of layout files. - * @throws InvalidParamException if the directory is invalid - */ - public function setLayoutPath($path) - { - $this->_layoutPath = Yii::getAlias($path); - } - - /** - * Defines path aliases. - * This method calls [[Yii::setAlias()]] to register the path aliases. - * This method is provided so that you can define path aliases when configuring a module. - * @property array list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * See [[setAliases()]] for an example. - * @param array $aliases list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * For example, - * - * ~~~ - * [ - * '@models' => '@app/models', // an existing alias - * '@backend' => __DIR__ . '/../backend', // a directory - * ] - * ~~~ - */ - public function setAliases($aliases) - { - foreach ($aliases as $name => $alias) { - Yii::setAlias($name, $alias); - } - } - - /** - * Checks whether the child module of the specified ID exists. - * This method supports checking the existence of both child and grand child modules. - * @param string $id module ID. For grand child modules, use ID path relative to this module (e.g. `admin/content`). - * @return boolean whether the named module exists. Both loaded and unloaded modules - * are considered. - */ - public function hasModule($id) - { - if (($pos = strpos($id, '/')) !== false) { - // sub-module - $module = $this->getModule(substr($id, 0, $pos)); - return $module === null ? false : $module->hasModule(substr($id, $pos + 1)); - } else { - return isset($this->_modules[$id]); - } - } - - /** - * Retrieves the child module of the specified ID. - * This method supports retrieving both child modules and grand child modules. - * @param string $id module ID (case-sensitive). To retrieve grand child modules, - * use ID path relative to this module (e.g. `admin/content`). - * @param boolean $load whether to load the module if it is not yet loaded. - * @return Module|null the module instance, null if the module does not exist. - * @see hasModule() - */ - public function getModule($id, $load = true) - { - if (($pos = strpos($id, '/')) !== false) { - // sub-module - $module = $this->getModule(substr($id, 0, $pos)); - return $module === null ? null : $module->getModule(substr($id, $pos + 1), $load); - } - - if (isset($this->_modules[$id])) { - if ($this->_modules[$id] instanceof Module) { - return $this->_modules[$id]; - } elseif ($load) { - Yii::trace("Loading module: $id", __METHOD__); - if (is_array($this->_modules[$id]) && !isset($this->_modules[$id]['class'])) { - $this->_modules[$id]['class'] = 'yii\base\Module'; - } - return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); - } - } - return null; - } - - /** - * Adds a sub-module to this module. - * @param string $id module ID - * @param Module|array|null $module the sub-module to be added to this module. This can - * be one of the followings: - * - * - a [[Module]] object - * - a configuration array: when [[getModule()]] is called initially, the array - * will be used to instantiate the sub-module - * - null: the named sub-module will be removed from this module - */ - public function setModule($id, $module) - { - if ($module === null) { - unset($this->_modules[$id]); - } else { - $this->_modules[$id] = $module; - } - } - - /** - * Returns the sub-modules in this module. - * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, - * then all sub-modules registered in this module will be returned, whether they are loaded or not. - * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. - * @return array the modules (indexed by their IDs) - */ - public function getModules($loadedOnly = false) - { - if ($loadedOnly) { - $modules = []; - foreach ($this->_modules as $module) { - if ($module instanceof Module) { - $modules[] = $module; - } - } - return $modules; - } else { - return $this->_modules; - } - } - - /** - * Registers sub-modules in the current module. - * - * Each sub-module should be specified as a name-value pair, where - * name refers to the ID of the module and value the module or a configuration - * array that can be used to create the module. In the latter case, [[Yii::createObject()]] - * will be used to create the module. - * - * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for registering two sub-modules: - * - * ~~~ - * [ - * 'comment' => [ - * 'class' => 'app\modules\comment\CommentModule', - * 'db' => 'db', - * ], - * 'booking' => ['class' => 'app\modules\booking\BookingModule'], - * ] - * ~~~ - * - * @param array $modules modules (id => module configuration or instances) - */ - public function setModules($modules) - { - foreach ($modules as $id => $module) { - $this->_modules[$id] = $module; - } - } - - /** - * Checks whether the named component exists. - * @param string $id component ID - * @return boolean whether the named component exists. Both loaded and unloaded components - * are considered. - */ - public function hasComponent($id) - { - return isset($this->_components[$id]); - } - - /** - * Retrieves the named component. - * @param string $id component ID (case-sensitive) - * @param boolean $load whether to load the component if it is not yet loaded. - * @return Component|null the component instance, null if the component does not exist. - * @see hasComponent() - */ - public function getComponent($id, $load = true) - { - if (isset($this->_components[$id])) { - if ($this->_components[$id] instanceof Object) { - return $this->_components[$id]; - } elseif ($load) { - return $this->_components[$id] = Yii::createObject($this->_components[$id]); - } - } - return null; - } - - /** - * Registers a component with this module. - * @param string $id component ID - * @param Component|array|null $component the component to be registered with the module. This can - * be one of the followings: - * - * - a [[Component]] object - * - a configuration array: when [[getComponent()]] is called initially for this component, the array - * will be used to instantiate the component via [[Yii::createObject()]]. - * - null: the named component will be removed from the module - */ - public function setComponent($id, $component) - { - if ($component === null) { - unset($this->_components[$id]); - } else { - $this->_components[$id] = $component; - } - } - - /** - * Returns the registered components. - * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, - * then all components specified in the configuration will be returned, whether they are loaded or not. - * Loaded components will be returned as objects, while unloaded components as configuration arrays. - * @return array the components (indexed by their IDs) - */ - public function getComponents($loadedOnly = false) - { - if ($loadedOnly) { - $components = []; - foreach ($this->_components as $component) { - if ($component instanceof Component) { - $components[] = $component; - } - } - return $components; - } else { - return $this->_components; - } - } - - /** - * Registers a set of components in this module. - * - * Each component should be specified as a name-value pair, where - * name refers to the ID of the component and value the component or a configuration - * array that can be used to create the component. In the latter case, [[Yii::createObject()]] - * will be used to create the component. - * - * If a new component has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for setting two components: - * - * ~~~ - * [ - * 'db' => [ - * 'class' => 'yii\db\Connection', - * 'dsn' => 'sqlite:path/to/file.db', - * ], - * 'cache' => [ - * 'class' => 'yii\caching\DbCache', - * 'db' => 'db', - * ], - * ] - * ~~~ - * - * @param array $components components (id => component configuration or instance) - */ - public function setComponents($components) - { - foreach ($components as $id => $component) { - if (!is_object($component) && isset($this->_components[$id]['class']) && !isset($component['class'])) { - // set default component class - $component['class'] = $this->_components[$id]['class']; - } - $this->_components[$id] = $component; - } - } - - /** - * Loads components that are declared in [[preload]]. - * @throws InvalidConfigException if a component or module to be preloaded is unknown - */ - public function preloadComponents() - { - foreach ($this->preload as $id) { - if ($this->hasComponent($id)) { - $this->getComponent($id); - } elseif ($this->hasModule($id)) { - $this->getModule($id); - } else { - throw new InvalidConfigException("Unknown component or module: $id"); - } - } - } - - /** - * Runs a controller action specified by a route. - * This method parses the specified route and creates the corresponding child module(s), controller and action - * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. - * If the route is empty, the method will use [[defaultRoute]]. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @return mixed the result of the action. - * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully - */ - public function runAction($route, $params = []) - { - $parts = $this->createController($route); - if (is_array($parts)) { - /** @var Controller $controller */ - list($controller, $actionID) = $parts; - $oldController = Yii::$app->controller; - Yii::$app->controller = $controller; - $result = $controller->runAction($actionID, $params); - Yii::$app->controller = $oldController; - return $result; - } else { - $id = $this->getUniqueId(); - throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".'); - } - } - - /** - * Creates a controller instance based on the given route. - * - * The route should be relative to this module. The method implements the following algorithm - * to resolve the given route: - * - * 1. If the route is empty, use [[defaultRoute]]; - * 2. If the first segment of the route is a valid module ID as declared in [[modules]], - * call the module's `createController()` with the rest part of the route; - * 3. If the first segment of the route is found in [[controllerMap]], create a controller - * based on the corresponding configuration found in [[controllerMap]]; - * 4. The given route is in the format of `abc/def/xyz`. Try either `abc\DefController` - * or `abc\def\XyzController` class within the [[controllerNamespace|controller namespace]]. - * - * If any of the above steps resolves into a controller, it is returned together with the rest - * part of the route which will be treated as the action ID. Otherwise, false will be returned. - * - * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean If the controller is created successfully, it will be returned together - * with the requested action ID. Otherwise false will be returned. - * @throws InvalidConfigException if the controller class and its file do not match. - */ - public function createController($route) - { - if ($route === '') { - $route = $this->defaultRoute; - } - - // double slashes or leading/ending slashes may cause substr problem - $route = trim($route, '/'); - if (strpos($route, '//') !== false) { - return false; - } - - if (strpos($route, '/') !== false) { - list ($id, $route) = explode('/', $route, 2); - } else { - $id = $route; - $route = ''; - } - - // module and controller map take precedence - $module = $this->getModule($id); - if ($module !== null) { - return $module->createController($route); - } - if (isset($this->controllerMap[$id])) { - $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - return [$controller, $route]; - } - - if (($pos = strrpos($route, '/')) !== false) { - $id .= '/' . substr($route, 0, $pos); - $route = substr($route, $pos + 1); - } - - $controller = $this->createControllerByID($id); - if ($controller === null && $route !== '') { - $controller = $this->createControllerByID($id . '/' . $route); - $route = ''; - } - - return $controller === null ? false : [$controller, $route]; - } - - /** - * Creates a controller based on the given controller ID. - * - * The controller ID is relative to this module. The controller class - * should be namespaced under [[controllerNamespace]]. - * - * Note that this method does not check [[modules]] or [[controllerMap]]. - * - * @param string $id the controller ID - * @return Controller the newly created controller instance, or null if the controller ID is invalid. - * @throws InvalidConfigException if the controller class and its file name do not match. - * This exception is only thrown when in debug mode. - */ - public function createControllerByID($id) - { - if (!preg_match('%^[a-z0-9\\-_/]+$%', $id)) { - return null; - } - - $pos = strrpos($id, '/'); - if ($pos === false) { - $prefix = ''; - $className = $id; - } else { - $prefix = substr($id, 0, $pos + 1); - $className = substr($id, $pos + 1); - } - - $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $className))) . 'Controller'; - $className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\'); - if (strpos($className, '-') !== false || !class_exists($className)) { - return null; - } - - if (is_subclass_of($className, 'yii\base\Controller')) { - return new $className($id, $this); - } elseif (YII_DEBUG) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } else { - return null; - } - } - - /** - * This method is invoked right before an action of this module is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * Make sure you call the parent implementation so that the relevant event is triggered. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - return true; - } - - /** - * This method is invoked right after an action of this module has been executed. - * You may override this method to do some postprocessing for the action. - * Make sure you call the parent implementation so that the relevant event is triggered. - * Also make sure you return the action result, whether it is processed or not. - * @param Action $action the action just executed. - * @param mixed $result the action return result. - * @return mixed the processed action result. - */ - public function afterAction($action, $result) - { - return $result; - } + /** + * @var array custom module parameters (name => value). + */ + public $params = []; + /** + * @var array the IDs of the components or modules that should be preloaded right after initialization. + */ + public $preload = []; + /** + * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. + */ + public $id; + /** + * @var Module the parent module of this module. Null if this module does not have a parent. + */ + public $module; + /** + * @var string|boolean the layout that should be applied for views within this module. This refers to a view name + * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] + * will be taken. If this is false, layout will be disabled within this module. + */ + public $layout; + /** + * @var array mapping from controller ID to controller configurations. + * Each name-value pair specifies the configuration of a single controller. + * A controller configuration can be either a string or an array. + * If the former, the string should be the fully qualified class name of the controller. + * If the latter, the array must contain a 'class' element which specifies + * the controller's fully qualified class name, and the rest of the name-value pairs + * in the array are used to initialize the corresponding controller properties. For example, + * + * ~~~ + * [ + * 'account' => 'app\controllers\UserController', + * 'article' => [ + * 'class' => 'app\controllers\PostController', + * 'pageTitle' => 'something new', + * ], + * ] + * ~~~ + */ + public $controllerMap = []; + /** + * @var string the namespace that controller classes are in. If not set, + * it will use the "controllers" sub-namespace under the namespace of this module. + * For example, if the namespace of this module is "foo\bar", then the default + * controller namespace would be "foo\bar\controllers". + */ + public $controllerNamespace; + /** + * @return string the default route of this module. Defaults to 'default'. + * The route may consist of child module ID, controller ID, and/or action ID. + * For example, `help`, `post/create`, `admin/post/create`. + * If action ID is not given, it will take the default value as specified in + * [[Controller::defaultAction]]. + */ + public $defaultRoute = 'default'; + /** + * @var string the root directory of the module. + */ + private $_basePath; + /** + * @var string the root directory that contains view files for this module + */ + private $_viewPath; + /** + * @var string the root directory that contains layout view files for this module. + */ + private $_layoutPath; + /** + * @var array child modules of this module + */ + private $_modules = []; + /** + * @var array components registered under this module + */ + private $_components = []; + + /** + * Constructor. + * @param string $id the ID of this module + * @param Module $parent the parent module (if any) + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $parent = null, $config = []) + { + $this->id = $id; + $this->module = $parent; + parent::__construct($config); + } + + /** + * Getter magic method. + * This method is overridden to support accessing components + * like reading module properties. + * @param string $name component or property name + * @return mixed the named property value + */ + public function __get($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name); + } else { + return parent::__get($name); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named component is loaded. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name) !== null; + } else { + return parent::__isset($name); + } + } + + /** + * Initializes the module. + * This method is called after the module is created and initialized with property values + * given in configuration. The default implementation will call [[preloadComponents()]] to + * load components that are declared in [[preload]]. + * + * If you override this method, please make sure you call the parent implementation. + */ + public function init() + { + if ($this->controllerNamespace === null) { + $class = get_class($this); + if (($pos = strrpos($class, '\\')) !== false) { + $this->controllerNamespace = substr($class, 0, $pos) . '\\controllers'; + } + } + $this->preloadComponents(); + } + + /** + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Note that if the module is an application, an empty string will be returned. + * @return string the unique ID of the module. + */ + public function getUniqueId() + { + return $this->module ? ltrim($this->module->getUniqueId() . '/' . $this->id, '/') : $this->id; + } + + /** + * Returns the root directory of the module. + * It defaults to the directory containing the module class file. + * @return string the root directory of the module. + */ + public function getBasePath() + { + if ($this->_basePath === null) { + $class = new \ReflectionClass($this); + $this->_basePath = dirname($class->getFileName()); + } + + return $this->_basePath; + } + + /** + * Sets the root directory of the module. + * This method can only be invoked at the beginning of the constructor. + * @param string $path the root directory of the module. This can be either a directory name or a path alias. + * @throws InvalidParamException if the directory does not exist. + */ + public function setBasePath($path) + { + $path = Yii::getAlias($path); + $p = realpath($path); + if ($p !== false && is_dir($p)) { + $this->_basePath = $p; + } else { + throw new InvalidParamException("The directory does not exist: $path"); + } + } + + /** + * Returns the directory that contains the controller classes according to [[controllerNamespace]]. + * Note that in order for this method to return a value, you must define + * an alias for the root namespace of [[controllerNamespace]]. + * @return string the directory that contains the controller classes. + * @throws InvalidParamException if there is no alias defined for the root namespace of [[controllerNamespace]]. + */ + public function getControllerPath() + { + return Yii::getAlias('@' . str_replace('\\', '/', $this->controllerNamespace)); + } + + /** + * Returns the directory that contains the view files for this module. + * @return string the root directory of view files. Defaults to "[[basePath]]/view". + */ + public function getViewPath() + { + if ($this->_viewPath !== null) { + return $this->_viewPath; + } else { + return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; + } + } + + /** + * Sets the directory that contains the view files. + * @param string $path the root directory of view files. + * @throws InvalidParamException if the directory is invalid + */ + public function setViewPath($path) + { + $this->_viewPath = Yii::getAlias($path); + } + + /** + * Returns the directory that contains layout view files for this module. + * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". + */ + public function getLayoutPath() + { + if ($this->_layoutPath !== null) { + return $this->_layoutPath; + } else { + return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; + } + } + + /** + * Sets the directory that contains the layout files. + * @param string $path the root directory of layout files. + * @throws InvalidParamException if the directory is invalid + */ + public function setLayoutPath($path) + { + $this->_layoutPath = Yii::getAlias($path); + } + + /** + * Defines path aliases. + * This method calls [[Yii::setAlias()]] to register the path aliases. + * This method is provided so that you can define path aliases when configuring a module. + * @property array list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * See [[setAliases()]] for an example. + * @param array $aliases list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * For example, + * + * ~~~ + * [ + * '@models' => '@app/models', // an existing alias + * '@backend' => __DIR__ . '/../backend', // a directory + * ] + * ~~~ + */ + public function setAliases($aliases) + { + foreach ($aliases as $name => $alias) { + Yii::setAlias($name, $alias); + } + } + + /** + * Checks whether the child module of the specified ID exists. + * This method supports checking the existence of both child and grand child modules. + * @param string $id module ID. For grand child modules, use ID path relative to this module (e.g. `admin/content`). + * @return boolean whether the named module exists. Both loaded and unloaded modules + * are considered. + */ + public function hasModule($id) + { + if (($pos = strpos($id, '/')) !== false) { + // sub-module + $module = $this->getModule(substr($id, 0, $pos)); + + return $module === null ? false : $module->hasModule(substr($id, $pos + 1)); + } else { + return isset($this->_modules[$id]); + } + } + + /** + * Retrieves the child module of the specified ID. + * This method supports retrieving both child modules and grand child modules. + * @param string $id module ID (case-sensitive). To retrieve grand child modules, + * use ID path relative to this module (e.g. `admin/content`). + * @param boolean $load whether to load the module if it is not yet loaded. + * @return Module|null the module instance, null if the module does not exist. + * @see hasModule() + */ + public function getModule($id, $load = true) + { + if (($pos = strpos($id, '/')) !== false) { + // sub-module + $module = $this->getModule(substr($id, 0, $pos)); + + return $module === null ? null : $module->getModule(substr($id, $pos + 1), $load); + } + + if (isset($this->_modules[$id])) { + if ($this->_modules[$id] instanceof Module) { + return $this->_modules[$id]; + } elseif ($load) { + Yii::trace("Loading module: $id", __METHOD__); + if (is_array($this->_modules[$id]) && !isset($this->_modules[$id]['class'])) { + $this->_modules[$id]['class'] = 'yii\base\Module'; + } + + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); + } + } + + return null; + } + + /** + * Adds a sub-module to this module. + * @param string $id module ID + * @param Module|array|null $module the sub-module to be added to this module. This can + * be one of the followings: + * + * - a [[Module]] object + * - a configuration array: when [[getModule()]] is called initially, the array + * will be used to instantiate the sub-module + * - null: the named sub-module will be removed from this module + */ + public function setModule($id, $module) + { + if ($module === null) { + unset($this->_modules[$id]); + } else { + $this->_modules[$id] = $module; + } + } + + /** + * Returns the sub-modules in this module. + * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, + * then all sub-modules registered in this module will be returned, whether they are loaded or not. + * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. + * @return array the modules (indexed by their IDs) + */ + public function getModules($loadedOnly = false) + { + if ($loadedOnly) { + $modules = []; + foreach ($this->_modules as $module) { + if ($module instanceof Module) { + $modules[] = $module; + } + } + + return $modules; + } else { + return $this->_modules; + } + } + + /** + * Registers sub-modules in the current module. + * + * Each sub-module should be specified as a name-value pair, where + * name refers to the ID of the module and value the module or a configuration + * array that can be used to create the module. In the latter case, [[Yii::createObject()]] + * will be used to create the module. + * + * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for registering two sub-modules: + * + * ~~~ + * [ + * 'comment' => [ + * 'class' => 'app\modules\comment\CommentModule', + * 'db' => 'db', + * ], + * 'booking' => ['class' => 'app\modules\booking\BookingModule'], + * ] + * ~~~ + * + * @param array $modules modules (id => module configuration or instances) + */ + public function setModules($modules) + { + foreach ($modules as $id => $module) { + $this->_modules[$id] = $module; + } + } + + /** + * Checks whether the named component exists. + * @param string $id component ID + * @return boolean whether the named component exists. Both loaded and unloaded components + * are considered. + */ + public function hasComponent($id) + { + return isset($this->_components[$id]); + } + + /** + * Retrieves the named component. + * @param string $id component ID (case-sensitive) + * @param boolean $load whether to load the component if it is not yet loaded. + * @return Component|null the component instance, null if the component does not exist. + * @see hasComponent() + */ + public function getComponent($id, $load = true) + { + if (isset($this->_components[$id])) { + if ($this->_components[$id] instanceof Object) { + return $this->_components[$id]; + } elseif ($load) { + return $this->_components[$id] = Yii::createObject($this->_components[$id]); + } + } + + return null; + } + + /** + * Registers a component with this module. + * @param string $id component ID + * @param Component|array|null $component the component to be registered with the module. This can + * be one of the followings: + * + * - a [[Component]] object + * - a configuration array: when [[getComponent()]] is called initially for this component, the array + * will be used to instantiate the component via [[Yii::createObject()]]. + * - null: the named component will be removed from the module + */ + public function setComponent($id, $component) + { + if ($component === null) { + unset($this->_components[$id]); + } else { + $this->_components[$id] = $component; + } + } + + /** + * Returns the registered components. + * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, + * then all components specified in the configuration will be returned, whether they are loaded or not. + * Loaded components will be returned as objects, while unloaded components as configuration arrays. + * @return array the components (indexed by their IDs) + */ + public function getComponents($loadedOnly = false) + { + if ($loadedOnly) { + $components = []; + foreach ($this->_components as $component) { + if ($component instanceof Component) { + $components[] = $component; + } + } + + return $components; + } else { + return $this->_components; + } + } + + /** + * Registers a set of components in this module. + * + * Each component should be specified as a name-value pair, where + * name refers to the ID of the component and value the component or a configuration + * array that can be used to create the component. In the latter case, [[Yii::createObject()]] + * will be used to create the component. + * + * If a new component has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for setting two components: + * + * ~~~ + * [ + * 'db' => [ + * 'class' => 'yii\db\Connection', + * 'dsn' => 'sqlite:path/to/file.db', + * ], + * 'cache' => [ + * 'class' => 'yii\caching\DbCache', + * 'db' => 'db', + * ], + * ] + * ~~~ + * + * @param array $components components (id => component configuration or instance) + */ + public function setComponents($components) + { + foreach ($components as $id => $component) { + if (!is_object($component) && isset($this->_components[$id]['class']) && !isset($component['class'])) { + // set default component class + $component['class'] = $this->_components[$id]['class']; + } + $this->_components[$id] = $component; + } + } + + /** + * Loads components that are declared in [[preload]]. + * @throws InvalidConfigException if a component or module to be preloaded is unknown + */ + public function preloadComponents() + { + foreach ($this->preload as $id) { + if ($this->hasComponent($id)) { + $this->getComponent($id); + } elseif ($this->hasModule($id)) { + $this->getModule($id); + } else { + throw new InvalidConfigException("Unknown component or module: $id"); + } + } + } + + /** + * Runs a controller action specified by a route. + * This method parses the specified route and creates the corresponding child module(s), controller and action + * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. + * If the route is empty, the method will use [[defaultRoute]]. + * @param string $route the route that specifies the action. + * @param array $params the parameters to be passed to the action + * @return mixed the result of the action. + * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully + */ + public function runAction($route, $params = []) + { + $parts = $this->createController($route); + if (is_array($parts)) { + /** @var Controller $controller */ + list($controller, $actionID) = $parts; + $oldController = Yii::$app->controller; + Yii::$app->controller = $controller; + $result = $controller->runAction($actionID, $params); + Yii::$app->controller = $oldController; + + return $result; + } else { + $id = $this->getUniqueId(); + throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".'); + } + } + + /** + * Creates a controller instance based on the given route. + * + * The route should be relative to this module. The method implements the following algorithm + * to resolve the given route: + * + * 1. If the route is empty, use [[defaultRoute]]; + * 2. If the first segment of the route is a valid module ID as declared in [[modules]], + * call the module's `createController()` with the rest part of the route; + * 3. If the first segment of the route is found in [[controllerMap]], create a controller + * based on the corresponding configuration found in [[controllerMap]]; + * 4. The given route is in the format of `abc/def/xyz`. Try either `abc\DefController` + * or `abc\def\XyzController` class within the [[controllerNamespace|controller namespace]]. + * + * If any of the above steps resolves into a controller, it is returned together with the rest + * part of the route which will be treated as the action ID. Otherwise, false will be returned. + * + * @param string $route the route consisting of module, controller and action IDs. + * @return array|boolean If the controller is created successfully, it will be returned together + * with the requested action ID. Otherwise false will be returned. + * @throws InvalidConfigException if the controller class and its file do not match. + */ + public function createController($route) + { + if ($route === '') { + $route = $this->defaultRoute; + } + + // double slashes or leading/ending slashes may cause substr problem + $route = trim($route, '/'); + if (strpos($route, '//') !== false) { + return false; + } + + if (strpos($route, '/') !== false) { + list ($id, $route) = explode('/', $route, 2); + } else { + $id = $route; + $route = ''; + } + + // module and controller map take precedence + $module = $this->getModule($id); + if ($module !== null) { + return $module->createController($route); + } + if (isset($this->controllerMap[$id])) { + $controller = Yii::createObject($this->controllerMap[$id], $id, $this); + + return [$controller, $route]; + } + + if (($pos = strrpos($route, '/')) !== false) { + $id .= '/' . substr($route, 0, $pos); + $route = substr($route, $pos + 1); + } + + $controller = $this->createControllerByID($id); + if ($controller === null && $route !== '') { + $controller = $this->createControllerByID($id . '/' . $route); + $route = ''; + } + + return $controller === null ? false : [$controller, $route]; + } + + /** + * Creates a controller based on the given controller ID. + * + * The controller ID is relative to this module. The controller class + * should be namespaced under [[controllerNamespace]]. + * + * Note that this method does not check [[modules]] or [[controllerMap]]. + * + * @param string $id the controller ID + * @return Controller the newly created controller instance, or null if the controller ID is invalid. + * @throws InvalidConfigException if the controller class and its file name do not match. + * This exception is only thrown when in debug mode. + */ + public function createControllerByID($id) + { + if (!preg_match('%^[a-z0-9\\-_/]+$%', $id)) { + return null; + } + + $pos = strrpos($id, '/'); + if ($pos === false) { + $prefix = ''; + $className = $id; + } else { + $prefix = substr($id, 0, $pos + 1); + $className = substr($id, $pos + 1); + } + + $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $className))) . 'Controller'; + $className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\'); + if (strpos($className, '-') !== false || !class_exists($className)) { + return null; + } + + if (is_subclass_of($className, 'yii\base\Controller')) { + return new $className($id, $this); + } elseif (YII_DEBUG) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); + } else { + return null; + } + } + + /** + * This method is invoked right before an action of this module is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * Make sure you call the parent implementation so that the relevant event is triggered. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + return true; + } + + /** + * This method is invoked right after an action of this module has been executed. + * You may override this method to do some postprocessing for the action. + * Make sure you call the parent implementation so that the relevant event is triggered. + * Also make sure you return the action result, whether it is processed or not. + * @param Action $action the action just executed. + * @param mixed $result the action return result. + * @return mixed the processed action result. + */ + public function afterAction($action, $result) + { + return $result; + } } diff --git a/framework/base/NotSupportedException.php b/framework/base/NotSupportedException.php index 5123f435c0f..d5832c62209 100644 --- a/framework/base/NotSupportedException.php +++ b/framework/base/NotSupportedException.php @@ -15,11 +15,11 @@ */ class NotSupportedException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Not Supported'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Not Supported'; + } } diff --git a/framework/base/Object.php b/framework/base/Object.php index 8f20136c246..f6bfb4c18f5 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -76,212 +76,212 @@ */ class Object { - /** - * @return string the fully qualified name of this class. - */ - public static function className() - { - return get_called_class(); - } + /** + * @return string the fully qualified name of this class. + */ + public static function className() + { + return get_called_class(); + } - /** - * Constructor. - * The default implementation does two things: - * - * - Initializes the object with the given configuration `$config`. - * - Call [[init()]]. - * - * If this method is overridden in a child class, it is recommended that - * - * - the last parameter of the constructor is a configuration array, like `$config` here. - * - call the parent implementation at the end of the constructor. - * - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($config = []) - { - if (!empty($config)) { - Yii::configure($this, $config); - } - $this->init(); - } + /** + * Constructor. + * The default implementation does two things: + * + * - Initializes the object with the given configuration `$config`. + * - Call [[init()]]. + * + * If this method is overridden in a child class, it is recommended that + * + * - the last parameter of the constructor is a configuration array, like `$config` here. + * - call the parent implementation at the end of the constructor. + * + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($config = []) + { + if (!empty($config)) { + Yii::configure($this, $config); + } + $this->init(); + } - /** - * Initializes the object. - * This method is invoked at the end of the constructor after the object is initialized with the - * given configuration. - */ - public function init() - { - } + /** + * Initializes the object. + * This method is invoked at the end of the constructor after the object is initialized with the + * given configuration. + */ + public function init() + { + } - /** - * Returns the value of an object property. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `$value = $object->property;`. - * @param string $name the property name - * @return mixed the property value - * @throws UnknownPropertyException if the property is not defined - * @throws InvalidCallException if the property is write-only - * @see __set() - */ - public function __get($name) - { - $getter = 'get' . $name; - if (method_exists($this, $getter)) { - return $this->$getter(); - } elseif (method_exists($this, 'set' . $name)) { - throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); - } - } + /** + * Returns the value of an object property. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `$value = $object->property;`. + * @param string $name the property name + * @return mixed the property value + * @throws UnknownPropertyException if the property is not defined + * @throws InvalidCallException if the property is write-only + * @see __set() + */ + public function __get($name) + { + $getter = 'get' . $name; + if (method_exists($this, $getter)) { + return $this->$getter(); + } elseif (method_exists($this, 'set' . $name)) { + throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); + } else { + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); + } + } - /** - * Sets value of an object property. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `$object->property = $value;`. - * @param string $name the property name or the event name - * @param mixed $value the property value - * @throws UnknownPropertyException if the property is not defined - * @throws InvalidCallException if the property is read-only - * @see __get() - */ - public function __set($name, $value) - { - $setter = 'set' . $name; - if (method_exists($this, $setter)) { - $this->$setter($value); - } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); - } - } + /** + * Sets value of an object property. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `$object->property = $value;`. + * @param string $name the property name or the event name + * @param mixed $value the property value + * @throws UnknownPropertyException if the property is not defined + * @throws InvalidCallException if the property is read-only + * @see __get() + */ + public function __set($name, $value) + { + $setter = 'set' . $name; + if (method_exists($this, $setter)) { + $this->$setter($value); + } elseif (method_exists($this, 'get' . $name)) { + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); + } else { + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); + } + } - /** - * Checks if the named property is set (not null). - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `isset($object->property)`. - * - * Note that if the property is not defined, false will be returned. - * @param string $name the property name or the event name - * @return boolean whether the named property is set (not null). - */ - public function __isset($name) - { - $getter = 'get' . $name; - if (method_exists($this, $getter)) { - return $this->$getter() !== null; - } else { - return false; - } - } + /** + * Checks if the named property is set (not null). + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `isset($object->property)`. + * + * Note that if the property is not defined, false will be returned. + * @param string $name the property name or the event name + * @return boolean whether the named property is set (not null). + */ + public function __isset($name) + { + $getter = 'get' . $name; + if (method_exists($this, $getter)) { + return $this->$getter() !== null; + } else { + return false; + } + } - /** - * Sets an object property to null. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `unset($object->property)`. - * - * Note that if the property is not defined, this method will do nothing. - * If the property is read-only, it will throw an exception. - * @param string $name the property name - * @throws InvalidCallException if the property is read only. - */ - public function __unset($name) - { - $setter = 'set' . $name; - if (method_exists($this, $setter)) { - $this->$setter(null); - } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); - } - } + /** + * Sets an object property to null. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `unset($object->property)`. + * + * Note that if the property is not defined, this method will do nothing. + * If the property is read-only, it will throw an exception. + * @param string $name the property name + * @throws InvalidCallException if the property is read only. + */ + public function __unset($name) + { + $setter = 'set' . $name; + if (method_exists($this, $setter)) { + $this->$setter(null); + } elseif (method_exists($this, 'get' . $name)) { + throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); + } + } - /** - * Calls the named method which is not a class method. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when an unknown method is being invoked. - * @param string $name the method name - * @param array $params method parameters - * @throws UnknownMethodException when calling unknown method - * @return mixed the method return value - */ - public function __call($name, $params) - { - throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); - } + /** + * Calls the named method which is not a class method. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when an unknown method is being invoked. + * @param string $name the method name + * @param array $params method parameters + * @throws UnknownMethodException when calling unknown method + * @return mixed the method return value + */ + public function __call($name, $params) + { + throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); + } - /** - * Returns a value indicating whether a property is defined. - * A property is defined if: - * - * - the class has a getter or setter method associated with the specified name - * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVars` is true); - * - * @param string $name the property name - * @param boolean $checkVars whether to treat member variables as properties - * @return boolean whether the property is defined - * @see canGetProperty() - * @see canSetProperty() - */ - public function hasProperty($name, $checkVars = true) - { - return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false); - } + /** + * Returns a value indicating whether a property is defined. + * A property is defined if: + * + * - the class has a getter or setter method associated with the specified name + * (in this case, property name is case-insensitive); + * - the class has a member variable with the specified name (when `$checkVars` is true); + * + * @param string $name the property name + * @param boolean $checkVars whether to treat member variables as properties + * @return boolean whether the property is defined + * @see canGetProperty() + * @see canSetProperty() + */ + public function hasProperty($name, $checkVars = true) + { + return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false); + } - /** - * Returns a value indicating whether a property can be read. - * A property is readable if: - * - * - the class has a getter method associated with the specified name - * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVars` is true); - * - * @param string $name the property name - * @param boolean $checkVars whether to treat member variables as properties - * @return boolean whether the property can be read - * @see canSetProperty() - */ - public function canGetProperty($name, $checkVars = true) - { - return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name); - } + /** + * Returns a value indicating whether a property can be read. + * A property is readable if: + * + * - the class has a getter method associated with the specified name + * (in this case, property name is case-insensitive); + * - the class has a member variable with the specified name (when `$checkVars` is true); + * + * @param string $name the property name + * @param boolean $checkVars whether to treat member variables as properties + * @return boolean whether the property can be read + * @see canSetProperty() + */ + public function canGetProperty($name, $checkVars = true) + { + return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name); + } - /** - * Returns a value indicating whether a property can be set. - * A property is writable if: - * - * - the class has a setter method associated with the specified name - * (in this case, property name is case-insensitive); - * - the class has a member variable with the specified name (when `$checkVars` is true); - * - * @param string $name the property name - * @param boolean $checkVars whether to treat member variables as properties - * @return boolean whether the property can be written - * @see canGetProperty() - */ - public function canSetProperty($name, $checkVars = true) - { - return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name); - } + /** + * Returns a value indicating whether a property can be set. + * A property is writable if: + * + * - the class has a setter method associated with the specified name + * (in this case, property name is case-insensitive); + * - the class has a member variable with the specified name (when `$checkVars` is true); + * + * @param string $name the property name + * @param boolean $checkVars whether to treat member variables as properties + * @return boolean whether the property can be written + * @see canGetProperty() + */ + public function canSetProperty($name, $checkVars = true) + { + return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name); + } - /** - * Returns a value indicating whether a method is defined. - * - * The default implementation is a call to php function `method_exists()`. - * You may override this method when you implemented the php magic method `__call()`. - * @param string $name the property name - * @return boolean whether the property is defined - */ - public function hasMethod($name) - { - return method_exists($this, $name); - } + /** + * Returns a value indicating whether a method is defined. + * + * The default implementation is a call to php function `method_exists()`. + * You may override this method when you implemented the php magic method `__call()`. + * @param string $name the property name + * @return boolean whether the property is defined + */ + public function hasMethod($name) + { + return method_exists($this, $name); + } } diff --git a/framework/base/Request.php b/framework/base/Request.php index eb9f805f6d2..8f8531daba0 100644 --- a/framework/base/Request.php +++ b/framework/base/Request.php @@ -20,65 +20,66 @@ */ abstract class Request extends Component { - private $_scriptFile; - private $_isConsoleRequest; + private $_scriptFile; + private $_isConsoleRequest; - /** - * Resolves the current request into a route and the associated parameters. - * @return array the first element is the route, and the second is the associated parameters. - */ - abstract public function resolve(); + /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + */ + abstract public function resolve(); - /** - * Returns a value indicating whether the current request is made via command line - * @return boolean the value indicating whether the current request is made via console - */ - public function getIsConsoleRequest() - { - return $this->_isConsoleRequest !== null ? $this->_isConsoleRequest : PHP_SAPI === 'cli'; - } + /** + * Returns a value indicating whether the current request is made via command line + * @return boolean the value indicating whether the current request is made via console + */ + public function getIsConsoleRequest() + { + return $this->_isConsoleRequest !== null ? $this->_isConsoleRequest : PHP_SAPI === 'cli'; + } - /** - * Sets the value indicating whether the current request is made via command line - * @param boolean $value the value indicating whether the current request is made via command line - */ - public function setIsConsoleRequest($value) - { - $this->_isConsoleRequest = $value; - } + /** + * Sets the value indicating whether the current request is made via command line + * @param boolean $value the value indicating whether the current request is made via command line + */ + public function setIsConsoleRequest($value) + { + $this->_isConsoleRequest = $value; + } - /** - * Returns entry script file path. - * @return string entry script file path (processed w/ realpath()) - * @throws InvalidConfigException if the entry script file path cannot be determined automatically. - */ - public function getScriptFile() - { - if ($this->_scriptFile === null) { - if (isset($_SERVER['SCRIPT_FILENAME'])) { - $this->setScriptFile($_SERVER['SCRIPT_FILENAME']); - } else { - throw new InvalidConfigException('Unable to determine the entry script file path.'); - } - } - return $this->_scriptFile; - } + /** + * Returns entry script file path. + * @return string entry script file path (processed w/ realpath()) + * @throws InvalidConfigException if the entry script file path cannot be determined automatically. + */ + public function getScriptFile() + { + if ($this->_scriptFile === null) { + if (isset($_SERVER['SCRIPT_FILENAME'])) { + $this->setScriptFile($_SERVER['SCRIPT_FILENAME']); + } else { + throw new InvalidConfigException('Unable to determine the entry script file path.'); + } + } - /** - * Sets the entry script file path. - * The entry script file path can normally be determined based on the `SCRIPT_FILENAME` SERVER variable. - * However, for some server configurations, this may not be correct or feasible. - * This setter is provided so that the entry script file path can be manually specified. - * @param string $value the entry script file path. This can be either a file path or a path alias. - * @throws InvalidConfigException if the provided entry script file path is invalid. - */ - public function setScriptFile($value) - { - $scriptFile = realpath(Yii::getAlias($value)); - if ($scriptFile !== false && is_file($scriptFile)) { - $this->_scriptFile = $scriptFile; - } else { - throw new InvalidConfigException('Unable to determine the entry script file path.'); - } - } + return $this->_scriptFile; + } + + /** + * Sets the entry script file path. + * The entry script file path can normally be determined based on the `SCRIPT_FILENAME` SERVER variable. + * However, for some server configurations, this may not be correct or feasible. + * This setter is provided so that the entry script file path can be manually specified. + * @param string $value the entry script file path. This can be either a file path or a path alias. + * @throws InvalidConfigException if the provided entry script file path is invalid. + */ + public function setScriptFile($value) + { + $scriptFile = realpath(Yii::getAlias($value)); + if ($scriptFile !== false && is_file($scriptFile)) { + $this->_scriptFile = $scriptFile; + } else { + throw new InvalidConfigException('Unable to determine the entry script file path.'); + } + } } diff --git a/framework/base/Response.php b/framework/base/Response.php index 12ec644fab5..d258f0f5e06 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -15,29 +15,29 @@ */ class Response extends Component { - /** - * @var integer the exit status. Exit statuses should be in the range 0 to 254. - * The status 0 means the program terminates successfully. - */ - public $exitStatus = 0; + /** + * @var integer the exit status. Exit statuses should be in the range 0 to 254. + * The status 0 means the program terminates successfully. + */ + public $exitStatus = 0; - /** - * Sends the response to client. - */ - public function send() - { - } + /** + * Sends the response to client. + */ + public function send() + { + } - /** - * Removes all existing output buffers. - */ - public function clearOutputBuffers() - { - // the following manual level counting is to deal with zlib.output_compression set to On - for ($level = ob_get_level(); $level > 0; --$level) { - if (!@ob_end_clean()) { - ob_clean(); - } - } - } + /** + * Removes all existing output buffers. + */ + public function clearOutputBuffers() + { + // the following manual level counting is to deal with zlib.output_compression set to On + for ($level = ob_get_level(); $level > 0; --$level) { + if (!@ob_end_clean()) { + ob_clean(); + } + } + } } diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 63382ad6399..49de868defb 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -66,82 +66,82 @@ */ class Theme extends Component { - /** - * @var string the root path or path alias of this theme. All resources of this theme are located - * under this directory. This property must be set if [[pathMap]] is not set. - * @see pathMap - */ - public $basePath; - /** - * @var string the base URL (or path alias) for this theme. All resources of this theme are considered - * to be under this base URL. This property must be set. It is mainly used by [[getUrl()]]. - */ - public $baseUrl; - /** - * @var array the mapping between view directories and their corresponding themed versions. - * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. - * This property is used by [[applyTo()]] when a view is trying to apply the theme. - * Path aliases can be used when specifying directories. - */ - public $pathMap; + /** + * @var string the root path or path alias of this theme. All resources of this theme are located + * under this directory. This property must be set if [[pathMap]] is not set. + * @see pathMap + */ + public $basePath; + /** + * @var string the base URL (or path alias) for this theme. All resources of this theme are considered + * to be under this base URL. This property must be set. It is mainly used by [[getUrl()]]. + */ + public $baseUrl; + /** + * @var array the mapping between view directories and their corresponding themed versions. + * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. + * This property is used by [[applyTo()]] when a view is trying to apply the theme. + * Path aliases can be used when specifying directories. + */ + public $pathMap; + /** + * Initializes the theme. + * @throws InvalidConfigException if [[basePath]] is not set. + */ + public function init() + { + parent::init(); - /** - * Initializes the theme. - * @throws InvalidConfigException if [[basePath]] is not set. - */ - public function init() - { - parent::init(); + if ($this->baseUrl === null) { + throw new InvalidConfigException('The "baseUrl" property must be set.'); + } else { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } - if ($this->baseUrl === null) { - throw new InvalidConfigException('The "baseUrl" property must be set.'); - } else { - $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); - } + if (empty($this->pathMap)) { + if ($this->basePath !== null) { + $this->basePath = Yii::getAlias($this->basePath); + $this->pathMap = [Yii::$app->getBasePath() => [$this->basePath]]; + } else { + throw new InvalidConfigException('The "basePath" property must be set.'); + } + } + } - if (empty($this->pathMap)) { - if ($this->basePath !== null) { - $this->basePath = Yii::getAlias($this->basePath); - $this->pathMap = [Yii::$app->getBasePath() => [$this->basePath]]; - } else { - throw new InvalidConfigException('The "basePath" property must be set.'); - } - } - } + /** + * Converts a file to a themed file if possible. + * If there is no corresponding themed file, the original file will be returned. + * @param string $path the file to be themed + * @return string the themed file, or the original file if the themed version is not available. + */ + public function applyTo($path) + { + $path = FileHelper::normalizePath($path); + foreach ($this->pathMap as $from => $tos) { + $from = FileHelper::normalizePath(Yii::getAlias($from)) . DIRECTORY_SEPARATOR; + if (strpos($path, $from) === 0) { + $n = strlen($from); + foreach ((array) $tos as $to) { + $to = FileHelper::normalizePath(Yii::getAlias($to)) . DIRECTORY_SEPARATOR; + $file = $to . substr($path, $n); + if (is_file($file)) { + return $file; + } + } + } + } - /** - * Converts a file to a themed file if possible. - * If there is no corresponding themed file, the original file will be returned. - * @param string $path the file to be themed - * @return string the themed file, or the original file if the themed version is not available. - */ - public function applyTo($path) - { - $path = FileHelper::normalizePath($path); - foreach ($this->pathMap as $from => $tos) { - $from = FileHelper::normalizePath(Yii::getAlias($from)) . DIRECTORY_SEPARATOR; - if (strpos($path, $from) === 0) { - $n = strlen($from); - foreach ((array)$tos as $to) { - $to = FileHelper::normalizePath(Yii::getAlias($to)) . DIRECTORY_SEPARATOR; - $file = $to . substr($path, $n); - if (is_file($file)) { - return $file; - } - } - } - } - return $path; - } + return $path; + } - /** - * Converts a relative URL into an absolute URL using [[baseUrl]]. - * @param string $url the relative URL to be converted. - * @return string the absolute URL - */ - public function getUrl($url) - { - return $this->baseUrl . '/' . ltrim($url, '/'); - } + /** + * Converts a relative URL into an absolute URL using [[baseUrl]]. + * @param string $url the relative URL to be converted. + * @return string the absolute URL + */ + public function getUrl($url) + { + return $this->baseUrl . '/' . ltrim($url, '/'); + } } diff --git a/framework/base/UnknownClassException.php b/framework/base/UnknownClassException.php index b64c585c472..8f25353cdd5 100644 --- a/framework/base/UnknownClassException.php +++ b/framework/base/UnknownClassException.php @@ -15,11 +15,11 @@ */ class UnknownClassException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Unknown Class'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Unknown Class'; + } } diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 2277aff7e2b..5892a8f7844 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -15,11 +15,11 @@ */ class UnknownMethodException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Unknown Method'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Unknown Method'; + } } diff --git a/framework/base/UnknownPropertyException.php b/framework/base/UnknownPropertyException.php index 0a12ce1d504..8f7eeaaa841 100644 --- a/framework/base/UnknownPropertyException.php +++ b/framework/base/UnknownPropertyException.php @@ -15,11 +15,11 @@ */ class UnknownPropertyException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Unknown Property'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Unknown Property'; + } } diff --git a/framework/base/View.php b/framework/base/View.php index 67325660270..e0015314f73 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -26,453 +26,459 @@ */ class View extends Component { - /** - * @event Event an event that is triggered by [[beginPage()]]. - */ - const EVENT_BEGIN_PAGE = 'beginPage'; - /** - * @event Event an event that is triggered by [[endPage()]]. - */ - const EVENT_END_PAGE = 'endPage'; - /** - * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. - */ - const EVENT_BEFORE_RENDER = 'beforeRender'; - /** - * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file. - */ - const EVENT_AFTER_RENDER = 'afterRender'; - - /** - * @var ViewContextInterface the context under which the [[renderFile()]] method is being invoked. - */ - public $context; - /** - * @var mixed custom parameters that are shared among view templates. - */ - public $params = []; - /** - * @var array a list of available renderers indexed by their corresponding supported file extensions. - * Each renderer may be a view renderer object or the configuration for creating the renderer object. - * For example, the following configuration enables both Smarty and Twig view renderers: - * - * ~~~ - * [ - * 'tpl' => ['class' => 'yii\smarty\ViewRenderer'], - * 'twig' => ['class' => 'yii\twig\ViewRenderer'], - * ] - * ~~~ - * - * If no renderer is available for the given view file, the view file will be treated as a normal PHP - * and rendered via [[renderPhpFile()]]. - */ - public $renderers; - /** - * @var string the default view file extension. This will be appended to view file names if they don't have file extensions. - */ - public $defaultExtension = 'php'; - /** - * @var Theme|array the theme object or the configuration array for creating the theme object. - * If not set, it means theming is not enabled. - */ - public $theme; - /** - * @var array a list of named output blocks. The keys are the block names and the values - * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] - * to capture small fragments of a view. They can be later accessed somewhere else - * through this property. - */ - public $blocks; - /** - * @var array a list of currently active fragment cache widgets. This property - * is used internally to implement the content caching feature. Do not modify it directly. - * @internal - */ - public $cacheStack = []; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it directly. - * @internal - */ - public $dynamicPlaceholders = []; - - /** - * @var array the view files currently being rendered. There may be multiple view files being - * rendered at a moment because one may render a view file within another. - */ - private $_viewFiles = []; - - - /** - * Initializes the view component. - */ - public function init() - { - parent::init(); - if (is_array($this->theme)) { - if (!isset($this->theme['class'])) { - $this->theme['class'] = 'yii\base\Theme'; - } - $this->theme = Yii::createObject($this->theme); - } - } - - /** - * Renders a view. - * - * The view to be rendered can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]]. - * - resolving any other format will be performed via [[ViewContext::findViewFile()]]. - * - * @param string $view the view name. Please refer to [[Controller::findViewFile()]] - * and [[Widget::findViewFile()]] on how to specify this parameter. - * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param object $context the context that the view should use for rendering the view. If null, - * existing [[context]] will be used. - * @return string the rendering result - * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. - * @see renderFile() - */ - public function render($view, $params = [], $context = null) - { - $viewFile = $this->findViewFile($view, $context); - return $this->renderFile($viewFile, $params, $context); - } - - /** - * Finds the view file based on the given view name. - * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] - * on how to specify this parameter. - * @param object $context the context that the view should be used to search the view file. If null, - * existing [[context]] will be used. - * @return string the view file path. Note that the file may not exist. - * @throws InvalidCallException if [[context]] is required and invalid. - */ - protected function findViewFile($view, $context = null) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/main" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '//', 2) === 0) { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '/', 1) === 0) { - // e.g. "/site/index" - if (Yii::$app->controller !== null) { - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - throw new InvalidCallException("Unable to locate view file for view '$view': no active controller."); - } - } else { - // context required - if ($context === null) { - $context = $this->context; - } - if ($context instanceof ViewContextInterface) { - $file = $context->findViewFile($view); - } else { - throw new InvalidCallException("Unable to locate view file for view '$view': no active view context."); - } - } - - if (pathinfo($file, PATHINFO_EXTENSION) !== '') { - return $file; - } - $path = $file . '.' . $this->defaultExtension; - if ($this->defaultExtension !== 'php' && !is_file($path)) { - $path = $file . '.php'; - } - return $path; - } - - /** - * Renders a view file. - * - * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long - * as it is available. - * - * The method will call [[FileHelper::localize()]] to localize the view file. - * - * If [[renderer]] is enabled (not null), the method will use it to render the view file. - * Otherwise, it will simply include the view file as a normal PHP file, capture its output and - * return it as a string. - * - * @param string $viewFile the view file. This can be either a file path or a path alias. - * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param object $context the context that the view should use for rendering the view. If null, - * existing [[context]] will be used. - * @return string the rendering result - * @throws InvalidParamException if the view file does not exist - */ - public function renderFile($viewFile, $params = [], $context = null) - { - $viewFile = Yii::getAlias($viewFile); - if ($this->theme !== null) { - $viewFile = $this->theme->applyTo($viewFile); - } - if (is_file($viewFile)) { - $viewFile = FileHelper::localize($viewFile); - } else { - throw new InvalidParamException("The view file does not exist: $viewFile"); - } - - $oldContext = $this->context; - if ($context !== null) { - $this->context = $context; - } - $output = ''; - $this->_viewFiles[] = $viewFile; - - if ($this->beforeRender()) { - Yii::trace("Rendering view file: $viewFile", __METHOD__); - $ext = pathinfo($viewFile, PATHINFO_EXTENSION); - if (isset($this->renderers[$ext])) { - if (is_array($this->renderers[$ext]) || is_string($this->renderers[$ext])) { - $this->renderers[$ext] = Yii::createObject($this->renderers[$ext]); - } - /** @var ViewRenderer $renderer */ - $renderer = $this->renderers[$ext]; - $output = $renderer->render($this, $viewFile, $params); - } else { - $output = $this->renderPhpFile($viewFile, $params); - } - $this->afterRender($output); - } - - array_pop($this->_viewFiles); - $this->context = $oldContext; - - return $output; - } - - /** - * @return string|boolean the view file currently being rendered. False if no view file is being rendered. - */ - public function getViewFile() - { - return end($this->_viewFiles); - } - - /** - * This method is invoked right before [[renderFile()]] renders a view file. - * The default implementation will trigger the [[EVENT_BEFORE_RENDER]] event. - * If you override this method, make sure you call the parent implementation first. - * @return boolean whether to continue rendering the view file. - */ - public function beforeRender() - { - $event = new ViewEvent; - $this->trigger(self::EVENT_BEFORE_RENDER, $event); - return $event->isValid; - } - - /** - * This method is invoked right after [[renderFile()]] renders a view file. - * The default implementation will trigger the [[EVENT_AFTER_RENDER]] event. - * If you override this method, make sure you call the parent implementation first. - * @param string $output the rendering result of the view file. Updates to this parameter - * will be passed back and returned by [[renderFile()]]. - */ - public function afterRender(&$output) - { - if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER)) { - $event = new ViewEvent; - $event->output = $output; - $this->trigger(self::EVENT_AFTER_RENDER, $event); - $output = $event->output; - } - } - - /** - * Renders a view file as a PHP script. - * - * This method treats the view file as a PHP script and includes the file. - * It extracts the given parameters and makes them available in the view file. - * The method captures the output of the included view file and returns it as a string. - * - * This method should mainly be called by view renderer or [[renderFile()]]. - * - * @param string $_file_ the view file. - * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. - * @return string the rendering result - */ - public function renderPhpFile($_file_, $_params_ = []) - { - ob_start(); - ob_implicit_flush(false); - extract($_params_, EXTR_OVERWRITE); - require($_file_); - return ob_get_clean(); - } - - /** - * Renders dynamic content returned by the given PHP statements. - * This method is mainly used together with content caching (fragment caching and page caching) - * when some portions of the content (called *dynamic content*) should not be cached. - * The dynamic content must be returned by some PHP statements. - * @param string $statements the PHP statements for generating the dynamic content. - * @return string the placeholder of the dynamic content, or the dynamic content if there is no - * active content cache currently. - */ - public function renderDynamic($statements) - { - if (!empty($this->cacheStack)) { - $n = count($this->dynamicPlaceholders); - $placeholder = ""; - $this->addDynamicPlaceholder($placeholder, $statements); - return $placeholder; - } else { - return $this->evaluateDynamicContent($statements); - } - } - - /** - * Adds a placeholder for dynamic content. - * This method is internally used. - * @param string $placeholder the placeholder name - * @param string $statements the PHP statements for generating the dynamic content - */ - public function addDynamicPlaceholder($placeholder, $statements) - { - foreach ($this->cacheStack as $cache) { - $cache->dynamicPlaceholders[$placeholder] = $statements; - } - $this->dynamicPlaceholders[$placeholder] = $statements; - } - - /** - * Evaluates the given PHP statements. - * This method is mainly used internally to implement dynamic content feature. - * @param string $statements the PHP statements to be evaluated. - * @return mixed the return value of the PHP statements. - */ - public function evaluateDynamicContent($statements) - { - return eval($statements); - } - - /** - * Begins recording a block. - * This method is a shortcut to beginning [[Block]] - * @param string $id the block ID. - * @param boolean $renderInPlace whether to render the block content in place. - * Defaults to false, meaning the captured block will not be displayed. - * @return Block the Block widget instance - */ - public function beginBlock($id, $renderInPlace = false) - { - return Block::begin([ - 'id' => $id, - 'renderInPlace' => $renderInPlace, - 'view' => $this, - ]); - } - - /** - * Ends recording a block. - */ - public function endBlock() - { - Block::end(); - } - - /** - * Begins the rendering of content that is to be decorated by the specified view. - * This method can be used to implement nested layout. For example, a layout can be embedded - * in another layout file specified as '@app/views/layouts/base.php' like the following: - * - * ~~~ - * beginContent('@app/views/layouts/base.php'); ?> - * ...layout content here... - * endContent(); ?> - * ~~~ - * - * @param string $viewFile the view file that will be used to decorate the content enclosed by this widget. - * This can be specified as either the view file path or path alias. - * @param array $params the variables (name => value) to be extracted and made available in the decorative view. - * @return ContentDecorator the ContentDecorator widget instance - * @see ContentDecorator - */ - public function beginContent($viewFile, $params = []) - { - return ContentDecorator::begin([ - 'viewFile' => $viewFile, - 'params' => $params, - 'view' => $this, - ]); - } - - /** - * Ends the rendering of content. - */ - public function endContent() - { - ContentDecorator::end(); - } - - /** - * Begins fragment caching. - * This method will display cached content if it is available. - * If not, it will start caching and would expect an [[endCache()]] - * call to end the cache and save the content into cache. - * A typical usage of fragment caching is as follows, - * - * ~~~ - * if ($this->beginCache($id)) { - * // ...generate content here - * $this->endCache(); - * } - * ~~~ - * - * @param string $id a unique ID identifying the fragment to be cached. - * @param array $properties initial property values for [[FragmentCache]] - * @return boolean whether you should generate the content for caching. - * False if the cached version is available. - */ - public function beginCache($id, $properties = []) - { - $properties['id'] = $id; - $properties['view'] = $this; - /** @var FragmentCache $cache */ - $cache = FragmentCache::begin($properties); - if ($cache->getCachedContent() !== false) { - $this->endCache(); - return false; - } else { - return true; - } - } - - /** - * Ends fragment caching. - */ - public function endCache() - { - FragmentCache::end(); - } - - /** - * Marks the beginning of a page. - */ - public function beginPage() - { - ob_start(); - ob_implicit_flush(false); - - $this->trigger(self::EVENT_BEGIN_PAGE); - } - - /** - * Marks the ending of a page. - */ - public function endPage() - { - $this->trigger(self::EVENT_END_PAGE); - ob_end_flush(); - } + /** + * @event Event an event that is triggered by [[beginPage()]]. + */ + const EVENT_BEGIN_PAGE = 'beginPage'; + /** + * @event Event an event that is triggered by [[endPage()]]. + */ + const EVENT_END_PAGE = 'endPage'; + /** + * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. + */ + const EVENT_BEFORE_RENDER = 'beforeRender'; + /** + * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file. + */ + const EVENT_AFTER_RENDER = 'afterRender'; + + /** + * @var ViewContextInterface the context under which the [[renderFile()]] method is being invoked. + */ + public $context; + /** + * @var mixed custom parameters that are shared among view templates. + */ + public $params = []; + /** + * @var array a list of available renderers indexed by their corresponding supported file extensions. + * Each renderer may be a view renderer object or the configuration for creating the renderer object. + * For example, the following configuration enables both Smarty and Twig view renderers: + * + * ~~~ + * [ + * 'tpl' => ['class' => 'yii\smarty\ViewRenderer'], + * 'twig' => ['class' => 'yii\twig\ViewRenderer'], + * ] + * ~~~ + * + * If no renderer is available for the given view file, the view file will be treated as a normal PHP + * and rendered via [[renderPhpFile()]]. + */ + public $renderers; + /** + * @var string the default view file extension. This will be appended to view file names if they don't have file extensions. + */ + public $defaultExtension = 'php'; + /** + * @var Theme|array the theme object or the configuration array for creating the theme object. + * If not set, it means theming is not enabled. + */ + public $theme; + /** + * @var array a list of named output blocks. The keys are the block names and the values + * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] + * to capture small fragments of a view. They can be later accessed somewhere else + * through this property. + */ + public $blocks; + /** + * @var array a list of currently active fragment cache widgets. This property + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal + */ + public $cacheStack = []; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal + */ + public $dynamicPlaceholders = []; + + /** + * @var array the view files currently being rendered. There may be multiple view files being + * rendered at a moment because one may render a view file within another. + */ + private $_viewFiles = []; + + + /** + * Initializes the view component. + */ + public function init() + { + parent::init(); + if (is_array($this->theme)) { + if (!isset($this->theme['class'])) { + $this->theme['class'] = 'yii\base\Theme'; + } + $this->theme = Yii::createObject($this->theme); + } + } + + /** + * Renders a view. + * + * The view to be rendered can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]]. + * - resolving any other format will be performed via [[ViewContext::findViewFile()]]. + * + * @param string $view the view name. Please refer to [[Controller::findViewFile()]] + * and [[Widget::findViewFile()]] on how to specify this parameter. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. + * @return string the rendering result + * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. + * @see renderFile() + */ + public function render($view, $params = [], $context = null) + { + $viewFile = $this->findViewFile($view, $context); + + return $this->renderFile($viewFile, $params, $context); + } + + /** + * Finds the view file based on the given view name. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @param object $context the context that the view should be used to search the view file. If null, + * existing [[context]] will be used. + * @return string the view file path. Note that the file may not exist. + * @throws InvalidCallException if [[context]] is required and invalid. + */ + protected function findViewFile($view, $context = null) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0) { + // e.g. "/site/index" + if (Yii::$app->controller !== null) { + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + throw new InvalidCallException("Unable to locate view file for view '$view': no active controller."); + } + } else { + // context required + if ($context === null) { + $context = $this->context; + } + if ($context instanceof ViewContextInterface) { + $file = $context->findViewFile($view); + } else { + throw new InvalidCallException("Unable to locate view file for view '$view': no active view context."); + } + } + + if (pathinfo($file, PATHINFO_EXTENSION) !== '') { + return $file; + } + $path = $file . '.' . $this->defaultExtension; + if ($this->defaultExtension !== 'php' && !is_file($path)) { + $path = $file . '.php'; + } + + return $path; + } + + /** + * Renders a view file. + * + * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long + * as it is available. + * + * The method will call [[FileHelper::localize()]] to localize the view file. + * + * If [[renderer]] is enabled (not null), the method will use it to render the view file. + * Otherwise, it will simply include the view file as a normal PHP file, capture its output and + * return it as a string. + * + * @param string $viewFile the view file. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. + * @return string the rendering result + * @throws InvalidParamException if the view file does not exist + */ + public function renderFile($viewFile, $params = [], $context = null) + { + $viewFile = Yii::getAlias($viewFile); + if ($this->theme !== null) { + $viewFile = $this->theme->applyTo($viewFile); + } + if (is_file($viewFile)) { + $viewFile = FileHelper::localize($viewFile); + } else { + throw new InvalidParamException("The view file does not exist: $viewFile"); + } + + $oldContext = $this->context; + if ($context !== null) { + $this->context = $context; + } + $output = ''; + $this->_viewFiles[] = $viewFile; + + if ($this->beforeRender()) { + Yii::trace("Rendering view file: $viewFile", __METHOD__); + $ext = pathinfo($viewFile, PATHINFO_EXTENSION); + if (isset($this->renderers[$ext])) { + if (is_array($this->renderers[$ext]) || is_string($this->renderers[$ext])) { + $this->renderers[$ext] = Yii::createObject($this->renderers[$ext]); + } + /** @var ViewRenderer $renderer */ + $renderer = $this->renderers[$ext]; + $output = $renderer->render($this, $viewFile, $params); + } else { + $output = $this->renderPhpFile($viewFile, $params); + } + $this->afterRender($output); + } + + array_pop($this->_viewFiles); + $this->context = $oldContext; + + return $output; + } + + /** + * @return string|boolean the view file currently being rendered. False if no view file is being rendered. + */ + public function getViewFile() + { + return end($this->_viewFiles); + } + + /** + * This method is invoked right before [[renderFile()]] renders a view file. + * The default implementation will trigger the [[EVENT_BEFORE_RENDER]] event. + * If you override this method, make sure you call the parent implementation first. + * @return boolean whether to continue rendering the view file. + */ + public function beforeRender() + { + $event = new ViewEvent; + $this->trigger(self::EVENT_BEFORE_RENDER, $event); + + return $event->isValid; + } + + /** + * This method is invoked right after [[renderFile()]] renders a view file. + * The default implementation will trigger the [[EVENT_AFTER_RENDER]] event. + * If you override this method, make sure you call the parent implementation first. + * @param string $output the rendering result of the view file. Updates to this parameter + * will be passed back and returned by [[renderFile()]]. + */ + public function afterRender(&$output) + { + if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER)) { + $event = new ViewEvent; + $event->output = $output; + $this->trigger(self::EVENT_AFTER_RENDER, $event); + $output = $event->output; + } + } + + /** + * Renders a view file as a PHP script. + * + * This method treats the view file as a PHP script and includes the file. + * It extracts the given parameters and makes them available in the view file. + * The method captures the output of the included view file and returns it as a string. + * + * This method should mainly be called by view renderer or [[renderFile()]]. + * + * @param string $_file_ the view file. + * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. + * @return string the rendering result + */ + public function renderPhpFile($_file_, $_params_ = []) + { + ob_start(); + ob_implicit_flush(false); + extract($_params_, EXTR_OVERWRITE); + require($_file_); + + return ob_get_clean(); + } + + /** + * Renders dynamic content returned by the given PHP statements. + * This method is mainly used together with content caching (fragment caching and page caching) + * when some portions of the content (called *dynamic content*) should not be cached. + * The dynamic content must be returned by some PHP statements. + * @param string $statements the PHP statements for generating the dynamic content. + * @return string the placeholder of the dynamic content, or the dynamic content if there is no + * active content cache currently. + */ + public function renderDynamic($statements) + { + if (!empty($this->cacheStack)) { + $n = count($this->dynamicPlaceholders); + $placeholder = ""; + $this->addDynamicPlaceholder($placeholder, $statements); + + return $placeholder; + } else { + return $this->evaluateDynamicContent($statements); + } + } + + /** + * Adds a placeholder for dynamic content. + * This method is internally used. + * @param string $placeholder the placeholder name + * @param string $statements the PHP statements for generating the dynamic content + */ + public function addDynamicPlaceholder($placeholder, $statements) + { + foreach ($this->cacheStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + $this->dynamicPlaceholders[$placeholder] = $statements; + } + + /** + * Evaluates the given PHP statements. + * This method is mainly used internally to implement dynamic content feature. + * @param string $statements the PHP statements to be evaluated. + * @return mixed the return value of the PHP statements. + */ + public function evaluateDynamicContent($statements) + { + return eval($statements); + } + + /** + * Begins recording a block. + * This method is a shortcut to beginning [[Block]] + * @param string $id the block ID. + * @param boolean $renderInPlace whether to render the block content in place. + * Defaults to false, meaning the captured block will not be displayed. + * @return Block the Block widget instance + */ + public function beginBlock($id, $renderInPlace = false) + { + return Block::begin([ + 'id' => $id, + 'renderInPlace' => $renderInPlace, + 'view' => $this, + ]); + } + + /** + * Ends recording a block. + */ + public function endBlock() + { + Block::end(); + } + + /** + * Begins the rendering of content that is to be decorated by the specified view. + * This method can be used to implement nested layout. For example, a layout can be embedded + * in another layout file specified as '@app/views/layouts/base.php' like the following: + * + * ~~~ + * beginContent('@app/views/layouts/base.php'); ?> + * ...layout content here... + * endContent(); ?> + * ~~~ + * + * @param string $viewFile the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. + * @param array $params the variables (name => value) to be extracted and made available in the decorative view. + * @return ContentDecorator the ContentDecorator widget instance + * @see ContentDecorator + */ + public function beginContent($viewFile, $params = []) + { + return ContentDecorator::begin([ + 'viewFile' => $viewFile, + 'params' => $params, + 'view' => $this, + ]); + } + + /** + * Ends the rendering of content. + */ + public function endContent() + { + ContentDecorator::end(); + } + + /** + * Begins fragment caching. + * This method will display cached content if it is available. + * If not, it will start caching and would expect an [[endCache()]] + * call to end the cache and save the content into cache. + * A typical usage of fragment caching is as follows, + * + * ~~~ + * if ($this->beginCache($id)) { + * // ...generate content here + * $this->endCache(); + * } + * ~~~ + * + * @param string $id a unique ID identifying the fragment to be cached. + * @param array $properties initial property values for [[FragmentCache]] + * @return boolean whether you should generate the content for caching. + * False if the cached version is available. + */ + public function beginCache($id, $properties = []) + { + $properties['id'] = $id; + $properties['view'] = $this; + /** @var FragmentCache $cache */ + $cache = FragmentCache::begin($properties); + if ($cache->getCachedContent() !== false) { + $this->endCache(); + + return false; + } else { + return true; + } + } + + /** + * Ends fragment caching. + */ + public function endCache() + { + FragmentCache::end(); + } + + /** + * Marks the beginning of a page. + */ + public function beginPage() + { + ob_start(); + ob_implicit_flush(false); + + $this->trigger(self::EVENT_BEGIN_PAGE); + } + + /** + * Marks the ending of a page. + */ + public function endPage() + { + $this->trigger(self::EVENT_END_PAGE); + ob_end_flush(); + } } diff --git a/framework/base/ViewContextInterface.php b/framework/base/ViewContextInterface.php index 94f6751c65a..12cf348fdf3 100644 --- a/framework/base/ViewContextInterface.php +++ b/framework/base/ViewContextInterface.php @@ -17,10 +17,10 @@ */ interface ViewContextInterface { - /** - * Finds the view file corresponding to the specified relative view name. - * @param string $view a relative view name. The name does NOT start with a slash. - * @return string the view file path. Note that the file may not exist. - */ - public function findViewFile($view); + /** + * Finds the view file corresponding to the specified relative view name. + * @param string $view a relative view name. The name does NOT start with a slash. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view); } diff --git a/framework/base/ViewEvent.php b/framework/base/ViewEvent.php index bad7264b242..123f55ea8d2 100644 --- a/framework/base/ViewEvent.php +++ b/framework/base/ViewEvent.php @@ -15,17 +15,17 @@ */ class ViewEvent extends Event { - /** - * @var string the rendering result of [[View::renderFile()]]. - * Event handlers may modify this property and the modified output will be - * returned by [[View::renderFile()]]. This property is only used - * by [[View::EVENT_AFTER_RENDER]] event. - */ - public $output; - /** - * @var boolean whether to continue rendering the view file. Event handlers of - * [[View::EVENT_BEFORE_RENDER]] may set this property to decide whether - * to continue rendering the current view file. - */ - public $isValid = true; + /** + * @var string the rendering result of [[View::renderFile()]]. + * Event handlers may modify this property and the modified output will be + * returned by [[View::renderFile()]]. This property is only used + * by [[View::EVENT_AFTER_RENDER]] event. + */ + public $output; + /** + * @var boolean whether to continue rendering the view file. Event handlers of + * [[View::EVENT_BEFORE_RENDER]] may set this property to decide whether + * to continue rendering the current view file. + */ + public $isValid = true; } diff --git a/framework/base/ViewRenderer.php b/framework/base/ViewRenderer.php index 576bbe89bd4..1ce152e52fb 100644 --- a/framework/base/ViewRenderer.php +++ b/framework/base/ViewRenderer.php @@ -15,16 +15,16 @@ */ abstract class ViewRenderer extends Component { - /** - * Renders a view file. - * - * This method is invoked by [[View]] whenever it tries to render a view. - * Child classes must implement this method to render the given view file. - * - * @param View $view the view object used for rendering the file. - * @param string $file the view file. - * @param array $params the parameters to be passed to the view file. - * @return string the rendering result - */ - abstract public function render($view, $file, $params); + /** + * Renders a view file. + * + * This method is invoked by [[View]] whenever it tries to render a view. + * Child classes must implement this method to render the given view file. + * + * @param View $view the view object used for rendering the file. + * @param string $file the view file. + * @param array $params the parameters to be passed to the view file. + * @return string the rendering result + */ + abstract public function render($view, $file, $params); } diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 2f45689af00..7effec161c0 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -24,192 +24,197 @@ */ class Widget extends Component implements ViewContextInterface { - /** - * @var integer a counter used to generate [[id]] for widgets. - * @internal - */ - public static $counter = 0; - /** - * @var string the prefix to the automatically generated widget IDs. - * @see getId() - */ - public static $autoIdPrefix = 'w'; - - /** - * @var Widget[] the widgets that are currently being rendered (not ended). This property - * is maintained by [[begin()]] and [[end()]] methods. - * @internal - */ - public static $stack = []; - - - /** - * Begins a widget. - * This method creates an instance of the calling class. It will apply the configuration - * to the created instance. A matching [[end()]] call should be called later. - * @param array $config name-value pairs that will be used to initialize the object properties - * @return static the newly created widget instance - */ - public static function begin($config = []) - { - $config['class'] = get_called_class(); - /** @var Widget $widget */ - $widget = Yii::createObject($config); - self::$stack[] = $widget; - return $widget; - } - - /** - * Ends a widget. - * Note that the rendering result of the widget is directly echoed out. - * @return static the widget instance that is ended. - * @throws InvalidCallException if [[begin()]] and [[end()]] calls are not properly nested - */ - public static function end() - { - if (!empty(self::$stack)) { - $widget = array_pop(self::$stack); - if (get_class($widget) === get_called_class()) { - $widget->run(); - return $widget; - } else { - throw new InvalidCallException("Expecting end() of " . get_class($widget) . ", found " . get_called_class()); - } - } else { - throw new InvalidCallException("Unexpected " . get_called_class() . '::end() call. A matching begin() is not found.'); - } - } - - /** - * Creates a widget instance and runs it. - * The widget rendering result is returned by this method. - * @param array $config name-value pairs that will be used to initialize the object properties - * @return string the rendering result of the widget. - */ - public static function widget($config = []) - { - ob_start(); - ob_implicit_flush(false); - /** @var Widget $widget */ - $config['class'] = get_called_class(); - $widget = Yii::createObject($config); - $out = $widget->run(); - return ob_get_clean() . $out; - } - - private $_id; - - /** - * Returns the ID of the widget. - * @param boolean $autoGenerate whether to generate an ID if it is not set previously - * @return string ID of the widget. - */ - public function getId($autoGenerate = true) - { - if ($autoGenerate && $this->_id === null) { - $this->_id = self::$autoIdPrefix . self::$counter++; - } - return $this->_id; - } - - /** - * Sets the ID of the widget. - * @param string $value id of the widget. - */ - public function setId($value) - { - $this->_id = $value; - } - - private $_view; - - /** - * Returns the view object that can be used to render views or view files. - * The [[render()]] and [[renderFile()]] methods will use - * this view object to implement the actual view rendering. - * If not set, it will default to the "view" application component. - * @return \yii\web\View the view object that can be used to render views or view files. - */ - public function getView() - { - if ($this->_view === null) { - $this->_view = Yii::$app->getView(); - } - return $this->_view; - } - - /** - * Sets the view object to be used by this widget. - * @param View $view the view object that can be used to render views or view files. - */ - public function setView($view) - { - $this->_view = $view; - } - - /** - * Executes the widget. - * @return string the result of widget execution to be outputted. - */ - public function run() - { - } - - /** - * Renders a view. - * The view to be rendered can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. - * @param array $params the parameters (name-value pairs) that should be made available in the view. - * @return string the rendering result. - * @throws InvalidParamException if the view file does not exist. - */ - public function render($view, $params = []) - { - return $this->getView()->render($view, $params, $this); - } - - /** - * Renders a view file. - * @param string $file the view file to be rendered. This can be either a file path or a path alias. - * @param array $params the parameters (name-value pairs) that should be made available in the view. - * @return string the rendering result. - * @throws InvalidParamException if the view file does not exist. - */ - public function renderFile($file, $params = []) - { - return $this->getView()->renderFile($file, $params, $this); - } - - /** - * Returns the directory containing the view files for this widget. - * The default implementation returns the 'views' subdirectory under the directory containing the widget class file. - * @return string the directory containing the view files for this widget. - */ - public function getViewPath() - { - $class = new ReflectionClass($this); - return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; - } - - /** - * Finds the view file based on the given view name. - * File will be searched under [[viewPath]] directory. - * @param string $view the view name. - * @return string the view file path. Note that the file may not exist. - */ - public function findViewFile($view) - { - return $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } + /** + * @var integer a counter used to generate [[id]] for widgets. + * @internal + */ + public static $counter = 0; + /** + * @var string the prefix to the automatically generated widget IDs. + * @see getId() + */ + public static $autoIdPrefix = 'w'; + + /** + * @var Widget[] the widgets that are currently being rendered (not ended). This property + * is maintained by [[begin()]] and [[end()]] methods. + * @internal + */ + public static $stack = []; + + /** + * Begins a widget. + * This method creates an instance of the calling class. It will apply the configuration + * to the created instance. A matching [[end()]] call should be called later. + * @param array $config name-value pairs that will be used to initialize the object properties + * @return static the newly created widget instance + */ + public static function begin($config = []) + { + $config['class'] = get_called_class(); + /** @var Widget $widget */ + $widget = Yii::createObject($config); + self::$stack[] = $widget; + + return $widget; + } + + /** + * Ends a widget. + * Note that the rendering result of the widget is directly echoed out. + * @return static the widget instance that is ended. + * @throws InvalidCallException if [[begin()]] and [[end()]] calls are not properly nested + */ + public static function end() + { + if (!empty(self::$stack)) { + $widget = array_pop(self::$stack); + if (get_class($widget) === get_called_class()) { + $widget->run(); + + return $widget; + } else { + throw new InvalidCallException("Expecting end() of " . get_class($widget) . ", found " . get_called_class()); + } + } else { + throw new InvalidCallException("Unexpected " . get_called_class() . '::end() call. A matching begin() is not found.'); + } + } + + /** + * Creates a widget instance and runs it. + * The widget rendering result is returned by this method. + * @param array $config name-value pairs that will be used to initialize the object properties + * @return string the rendering result of the widget. + */ + public static function widget($config = []) + { + ob_start(); + ob_implicit_flush(false); + /** @var Widget $widget */ + $config['class'] = get_called_class(); + $widget = Yii::createObject($config); + $out = $widget->run(); + + return ob_get_clean() . $out; + } + + private $_id; + + /** + * Returns the ID of the widget. + * @param boolean $autoGenerate whether to generate an ID if it is not set previously + * @return string ID of the widget. + */ + public function getId($autoGenerate = true) + { + if ($autoGenerate && $this->_id === null) { + $this->_id = self::$autoIdPrefix . self::$counter++; + } + + return $this->_id; + } + + /** + * Sets the ID of the widget. + * @param string $value id of the widget. + */ + public function setId($value) + { + $this->_id = $value; + } + + private $_view; + + /** + * Returns the view object that can be used to render views or view files. + * The [[render()]] and [[renderFile()]] methods will use + * this view object to implement the actual view rendering. + * If not set, it will default to the "view" application component. + * @return \yii\web\View the view object that can be used to render views or view files. + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = Yii::$app->getView(); + } + + return $this->_view; + } + + /** + * Sets the view object to be used by this widget. + * @param View $view the view object that can be used to render views or view files. + */ + public function setView($view) + { + $this->_view = $view; + } + + /** + * Executes the widget. + * @return string the result of widget execution to be outputted. + */ + public function run() + { + } + + /** + * Renders a view. + * The view to be rendered can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ + public function render($view, $params = []) + { + return $this->getView()->render($view, $params, $this); + } + + /** + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ + public function renderFile($file, $params = []) + { + return $this->getView()->renderFile($file, $params, $this); + } + + /** + * Returns the directory containing the view files for this widget. + * The default implementation returns the 'views' subdirectory under the directory containing the widget class file. + * @return string the directory containing the view files for this widget. + */ + public function getViewPath() + { + $class = new ReflectionClass($this); + + return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; + } + + /** + * Finds the view file based on the given view name. + * File will be searched under [[viewPath]] directory. + * @param string $view the view name. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view) + { + return $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } } diff --git a/framework/behaviors/AttributeBehavior.php b/framework/behaviors/AttributeBehavior.php index 4f6430f2f00..eaf23200310 100644 --- a/framework/behaviors/AttributeBehavior.php +++ b/framework/behaviors/AttributeBehavior.php @@ -46,66 +46,65 @@ */ class AttributeBehavior extends Behavior { - /** - * @var array list of attributes that are to be automatically filled with the value specified via [[value]]. - * The array keys are the ActiveRecord events upon which the attributes are to be updated, - * and the array values are the corresponding attribute(s) to be updated. You can use a string to represent - * a single attribute, or an array to represent a list of attributes. For example, - * - * ```php - * [ - * ActiveRecord::EVENT_BEFORE_INSERT => ['attribute1', 'attribute2'], - * ActiveRecord::EVENT_BEFORE_UPDATE => 'attribute2', - * ] - * ``` - */ - public $attributes = []; - /** - * @var mixed the value that will be assigned to the current attributes. This can be an anonymous function - * or an arbitrary value. If the former, the return value of the function will be assigned to the attributes. - * The signature of the function should be as follows, - * - * ```php - * function ($event) { - * // return value will be assigned to the attribute - * } - * ``` - */ - public $value; + /** + * @var array list of attributes that are to be automatically filled with the value specified via [[value]]. + * The array keys are the ActiveRecord events upon which the attributes are to be updated, + * and the array values are the corresponding attribute(s) to be updated. You can use a string to represent + * a single attribute, or an array to represent a list of attributes. For example, + * + * ```php + * [ + * ActiveRecord::EVENT_BEFORE_INSERT => ['attribute1', 'attribute2'], + * ActiveRecord::EVENT_BEFORE_UPDATE => 'attribute2', + * ] + * ``` + */ + public $attributes = []; + /** + * @var mixed the value that will be assigned to the current attributes. This can be an anonymous function + * or an arbitrary value. If the former, the return value of the function will be assigned to the attributes. + * The signature of the function should be as follows, + * + * ```php + * function ($event) { + * // return value will be assigned to the attribute + * } + * ``` + */ + public $value; + /** + * @inheritdoc + */ + public function events() + { + return array_fill_keys(array_keys($this->attributes), 'evaluateAttributes'); + } - /** - * @inheritdoc - */ - public function events() - { - return array_fill_keys(array_keys($this->attributes), 'evaluateAttributes'); - } + /** + * Evaluates the attribute value and assigns it to the current attributes. + * @param $event + */ + public function evaluateAttributes($event) + { + if (!empty($this->attributes[$event->name])) { + $attributes = (array) $this->attributes[$event->name]; + $value = $this->getValue($event); + foreach ($attributes as $attribute) { + $this->owner->$attribute = $value; + } + } + } - /** - * Evaluates the attribute value and assigns it to the current attributes. - * @param $event - */ - public function evaluateAttributes($event) - { - if (!empty($this->attributes[$event->name])) { - $attributes = (array)$this->attributes[$event->name]; - $value = $this->getValue($event); - foreach ($attributes as $attribute) { - $this->owner->$attribute = $value; - } - } - } - - /** - * Returns the value of the current attributes. - * This method is called by [[evaluateAttributes()]]. Its return value will be assigned - * to the attributes corresponding to the triggering event. - * @param Event $event the event that triggers the current attribute updating. - * @return mixed the attribute value - */ - protected function getValue($event) - { - return $this->value instanceof Closure ? call_user_func($this->value, $event) : $this->value; - } + /** + * Returns the value of the current attributes. + * This method is called by [[evaluateAttributes()]]. Its return value will be assigned + * to the attributes corresponding to the triggering event. + * @param Event $event the event that triggers the current attribute updating. + * @return mixed the attribute value + */ + protected function getValue($event) + { + return $this->value instanceof Closure ? call_user_func($this->value, $event) : $this->value; + } } diff --git a/framework/behaviors/BlameableBehavior.php b/framework/behaviors/BlameableBehavior.php index 0a9dba9ccab..8584d9d4c1c 100644 --- a/framework/behaviors/BlameableBehavior.php +++ b/framework/behaviors/BlameableBehavior.php @@ -53,46 +53,47 @@ */ class BlameableBehavior extends AttributeBehavior { - /** - * @var array list of attributes that are to be automatically filled with the current user ID. - * The array keys are the ActiveRecord events upon which the attributes are to be filled with the user ID, - * and the array values are the corresponding attribute(s) to be updated. You can use a string to represent - * a single attribute, or an array to represent a list of attributes. - * The default setting is to update both of the `created_by` and `updated_by` attributes upon AR insertion, - * and update the `updated_by` attribute upon AR updating. - */ - public $attributes = [ - BaseActiveRecord::EVENT_BEFORE_INSERT => ['created_by', 'updated_by'], - BaseActiveRecord::EVENT_BEFORE_UPDATE => 'updated_by', - ]; - /** - * @var callable the value that will be assigned to the attributes. This should be a valid - * PHP callable whose return value will be assigned to the current attribute(s). - * The signature of the callable should be: - * - * ```php - * function ($event) { - * // return value will be assigned to the attribute(s) - * } - * ``` - * - * If this property is not set, the value of `Yii::$app->user->id` will be assigned to the attribute(s). - */ - public $value; + /** + * @var array list of attributes that are to be automatically filled with the current user ID. + * The array keys are the ActiveRecord events upon which the attributes are to be filled with the user ID, + * and the array values are the corresponding attribute(s) to be updated. You can use a string to represent + * a single attribute, or an array to represent a list of attributes. + * The default setting is to update both of the `created_by` and `updated_by` attributes upon AR insertion, + * and update the `updated_by` attribute upon AR updating. + */ + public $attributes = [ + BaseActiveRecord::EVENT_BEFORE_INSERT => ['created_by', 'updated_by'], + BaseActiveRecord::EVENT_BEFORE_UPDATE => 'updated_by', + ]; + /** + * @var callable the value that will be assigned to the attributes. This should be a valid + * PHP callable whose return value will be assigned to the current attribute(s). + * The signature of the callable should be: + * + * ```php + * function ($event) { + * // return value will be assigned to the attribute(s) + * } + * ``` + * + * If this property is not set, the value of `Yii::$app->user->id` will be assigned to the attribute(s). + */ + public $value; - /** - * Evaluates the value of the user. - * The return result of this method will be assigned to the current attribute(s). - * @param Event $event - * @return mixed the value of the user. - */ - protected function getValue($event) - { - if ($this->value === null) { - $user = Yii::$app->getUser(); - return $user && !$user->isGuest ? $user->id : null; - } else { - return call_user_func($this->value, $event); - } - } + /** + * Evaluates the value of the user. + * The return result of this method will be assigned to the current attribute(s). + * @param Event $event + * @return mixed the value of the user. + */ + protected function getValue($event) + { + if ($this->value === null) { + $user = Yii::$app->getUser(); + + return $user && !$user->isGuest ? $user->id : null; + } else { + return call_user_func($this->value, $event); + } + } } diff --git a/framework/behaviors/TimestampBehavior.php b/framework/behaviors/TimestampBehavior.php index 131348875de..7b5615f03bd 100644 --- a/framework/behaviors/TimestampBehavior.php +++ b/framework/behaviors/TimestampBehavior.php @@ -63,49 +63,48 @@ */ class TimestampBehavior extends AttributeBehavior { - /** - * @var array list of attributes that are to be automatically filled with timestamps. - * The array keys are the ActiveRecord events upon which the attributes are to be filled with timestamps, - * and the array values are the corresponding attribute(s) to be updated. You can use a string to represent - * a single attribute, or an array to represent a list of attributes. - * The default setting is to update both of the `created_at` and `updated_at` attributes upon AR insertion, - * and update the `updated_at` attribute upon AR updating. - */ - public $attributes = [ - BaseActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], - BaseActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at', - ]; - /** - * @var callable|Expression The expression that will be used for generating the timestamp. - * This can be either an anonymous function that returns the timestamp value, - * or an [[Expression]] object representing a DB expression (e.g. `new Expression('NOW()')`). - * If not set, it will use the value of `time()` to set the attributes. - */ - public $value; + /** + * @var array list of attributes that are to be automatically filled with timestamps. + * The array keys are the ActiveRecord events upon which the attributes are to be filled with timestamps, + * and the array values are the corresponding attribute(s) to be updated. You can use a string to represent + * a single attribute, or an array to represent a list of attributes. + * The default setting is to update both of the `created_at` and `updated_at` attributes upon AR insertion, + * and update the `updated_at` attribute upon AR updating. + */ + public $attributes = [ + BaseActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], + BaseActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at', + ]; + /** + * @var callable|Expression The expression that will be used for generating the timestamp. + * This can be either an anonymous function that returns the timestamp value, + * or an [[Expression]] object representing a DB expression (e.g. `new Expression('NOW()')`). + * If not set, it will use the value of `time()` to set the attributes. + */ + public $value; + /** + * @inheritdoc + */ + protected function getValue($event) + { + if ($this->value instanceof Expression) { + return $this->value; + } else { + return $this->value !== null ? call_user_func($this->value, $event) : time(); + } + } - /** - * @inheritdoc - */ - protected function getValue($event) - { - if ($this->value instanceof Expression) { - return $this->value; - } else { - return $this->value !== null ? call_user_func($this->value, $event) : time(); - } - } - - /** - * Updates a timestamp attribute to the current timestamp. - * - * ```php - * $model->touch('lastVisit'); - * ``` - * @param string $attribute the name of the attribute to update. - */ - public function touch($attribute) - { - $this->owner->updateAttributes(array_fill_keys((array)$attribute, $this->getValue(null))); - } + /** + * Updates a timestamp attribute to the current timestamp. + * + * ```php + * $model->touch('lastVisit'); + * ``` + * @param string $attribute the name of the attribute to update. + */ + public function touch($attribute) + { + $this->owner->updateAttributes(array_fill_keys((array) $attribute, $this->getValue(null))); + } } diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index 8a0d20763ae..db69f5be09d 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -20,114 +20,115 @@ */ class ApcCache extends Cache { - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - $key = $this->buildKey($key); - return apc_exists($key); - } + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - return apc_fetch($key); - } + return apc_exists($key); + } - /** - * Retrieves multiple values from cache with the specified keys. - * @param array $keys a list of keys identifying the cached values - * @return array a list of cached values indexed by the keys - */ - protected function getValues($keys) - { - return apc_fetch($keys); - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return apc_fetch($key); + } - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - return apc_store($key, $value, $expire); - } + /** + * Retrieves multiple values from cache with the specified keys. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + return apc_fetch($keys); + } - /** - * Stores multiple key-value pairs in cache. - * @param array $data array where key corresponds to cache key while value - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys - */ - protected function setValues($data, $expire) - { - return array_keys(apc_store($data, null, $expire)); - } + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + return apc_store($key, $value, $expire); + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - return apc_add($key, $value, $expire); - } + /** + * Stores multiple key-value pairs in cache. + * @param array $data array where key corresponds to cache key while value + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys + */ + protected function setValues($data, $expire) + { + return array_keys(apc_store($data, null, $expire)); + } - /** - * Adds multiple key-value pairs to cache. - * @param array $data array where key corresponds to cache key while value is the value stored - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys - */ - protected function addValues($data, $expire) - { - return array_keys(apc_add($data, null, $expire)); - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + return apc_add($key, $value, $expire); + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - return apc_delete($key); - } + /** + * Adds multiple key-value pairs to cache. + * @param array $data array where key corresponds to cache key while value is the value stored + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys + */ + protected function addValues($data, $expire) + { + return array_keys(apc_add($data, null, $expire)); + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - if (extension_loaded('apcu')) { - return apc_clear_cache(); - } else { - return apc_clear_cache('user'); - } - } + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return apc_delete($key); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + if (extension_loaded('apcu')) { + return apc_clear_cache(); + } else { + return apc_clear_cache('user'); + } + } } diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index 85d294edadc..e6e7083e12f 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -51,421 +51,431 @@ */ abstract class Cache extends Component implements \ArrayAccess { - /** - * @var string a string prefixed to every cache key so that it is unique. If not set, - * it will use a prefix generated from [[\yii\base\Application::id]]. You may set this property to be an empty string - * if you don't want to use key prefix. It is recommended that you explicitly set this property to some - * static value if the cached data needs to be shared among multiple applications. - * - * To ensure interoperability, only alphanumeric characters should be used. - */ - public $keyPrefix; - /** - * @var array|boolean the functions used to serialize and unserialize cached data. Defaults to null, meaning - * using the default PHP `serialize()` and `unserialize()` functions. If you want to use some more efficient - * serializer (e.g. [igbinary](http://pecl.php.net/package/igbinary)), you may configure this property with - * a two-element array. The first element specifies the serialization function, and the second the deserialization - * function. If this property is set false, data will be directly sent to and retrieved from the underlying - * cache component without any serialization or deserialization. You should not turn off serialization if - * you are using [[Dependency|cache dependency]], because it relies on data serialization. - */ - public $serializer; - - - /** - * Initializes the application component. - * This method overrides the parent implementation by setting default cache key prefix. - */ - public function init() - { - parent::init(); - if ($this->keyPrefix === null) { - $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); - } - } - - /** - * Builds a normalized cache key from a given key. - * - * If the given key is a string containing alphanumeric characters only and no more than 32 characters, - * then the key will be returned back prefixed with [[keyPrefix]]. Otherwise, a normalized key - * is generated by serializing the given key, applying MD5 hashing, and prefixing with [[keyPrefix]]. - * - * @param mixed $key the key to be normalized - * @return string the generated cache key - */ - protected function buildKey($key) - { - if (is_string($key)) { - $key = ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key); - } else { - $key = md5(json_encode($key)); - } - return $this->keyPrefix . $key; - } - - /** - * Retrieves a value from cache with a specified key. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return mixed the value stored in cache, false if the value is not in the cache, expired, - * or the dependency associated with the cached data has changed. - */ - public function get($key) - { - $key = $this->buildKey($key); - $value = $this->getValue($key); - if ($value === false || $this->serializer === false) { - return $value; - } elseif ($this->serializer === null) { - $value = unserialize($value); - } else { - $value = call_user_func($this->serializer[1], $value); - } - if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged($this))) { - return $value[0]; - } else { - return false; - } - } - - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * In case a cache does not support this feature natively, this method will try to simulate it - * but has no performance improvement over getting it. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - $key = $this->buildKey($key); - $value = $this->getValue($key); - return $value !== false; - } - - /** - * Retrieves multiple values from cache with the specified keys. - * Some caches (such as memcache, apc) allow retrieving multiple cached values at the same time, - * which may improve the performance. In case a cache does not support this feature natively, - * this method will try to simulate it. - * @param array $keys list of keys identifying the cached values - * @return array list of cached values corresponding to the specified keys. The array - * is returned in terms of (key, value) pairs. - * If a value is not cached or expired, the corresponding array value will be false. - */ - public function mget($keys) - { - $keyMap = []; - foreach ($keys as $key) { - $keyMap[$key] = $this->buildKey($key); - } - $values = $this->getValues(array_values($keyMap)); - $results = []; - foreach ($keyMap as $key => $newKey) { - $results[$key] = false; - if (isset($values[$newKey])) { - if ($this->serializer === false) { - $results[$key] = $values[$newKey]; - } else { - $value = $this->serializer === null ? unserialize($values[$newKey]) - : call_user_func($this->serializer[1], $values[$newKey]); - - if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged($this))) { - $results[$key] = $value[0]; - } - } - } - } - return $results; - } - - /** - * Stores a value identified by a key into cache. - * If the cache already contains such a key, the existing value and - * expiration time will be replaced with the new ones, respectively. - * - * @param mixed $key a key identifying the value to be cached. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @param mixed $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @param Dependency $dependency dependency of the cached item. If the dependency changes, - * the corresponding value in the cache will be invalidated when it is fetched via [[get()]]. - * This parameter is ignored if [[serializer]] is false. - * @return boolean whether the value is successfully stored into cache - */ - public function set($key, $value, $expire = 0, $dependency = null) - { - if ($dependency !== null && $this->serializer !== false) { - $dependency->evaluateDependency($this); - } - if ($this->serializer === null) { - $value = serialize([$value, $dependency]); - } elseif ($this->serializer !== false) { - $value = call_user_func($this->serializer[0], [$value, $dependency]); - } - $key = $this->buildKey($key); - return $this->setValue($key, $value, $expire); - } - - /** - * Stores multiple items in cache. Each item contains a value identified by a key. - * If the cache already contains such a key, the existing value and - * expiration time will be replaced with the new ones, respectively. - * - * @param array $items the items to be cached, as key-value pairs. - * @param integer $expire default number of seconds in which the cached values will expire. 0 means never expire. - * @param Dependency $dependency dependency of the cached items. If the dependency changes, - * the corresponding values in the cache will be invalidated when it is fetched via [[get()]]. - * This parameter is ignored if [[serializer]] is false. - * @return boolean whether the items are successfully stored into cache - */ - public function mset($items, $expire = 0, $dependency = null) - { - if ($dependency !== null && $this->serializer !== false) { - $dependency->evaluateDependency($this); - } - - $data = []; - foreach ($items as $key => $value) { - if ($this->serializer === null) { - $value = serialize([$value, $dependency]); - } elseif ($this->serializer !== false) { - $value = call_user_func($this->serializer[0], [$value, $dependency]); - } - - $key = $this->buildKey($key); - $data[$key] = $value; - } - return $this->setValues($data, $expire); - } - - /** - * Stores multiple items in cache. Each item contains a value identified by a key. - * If the cache already contains such a key, the existing value and expiration time will be preserved. - * - * @param array $items the items to be cached, as key-value pairs. - * @param integer $expire default number of seconds in which the cached values will expire. 0 means never expire. - * @param Dependency $dependency dependency of the cached items. If the dependency changes, - * the corresponding values in the cache will be invalidated when it is fetched via [[get()]]. - * This parameter is ignored if [[serializer]] is false. - * @return boolean whether the items are successfully stored into cache - */ - public function madd($items, $expire = 0, $dependency = null) - { - if ($dependency !== null && $this->serializer !== false) { - $dependency->evaluateDependency($this); - } - - $data = []; - foreach ($items as $key => $value) { - if ($this->serializer === null) { - $value = serialize([$value, $dependency]); - } elseif ($this->serializer !== false) { - $value = call_user_func($this->serializer[0], [$value, $dependency]); - } - - $key = $this->buildKey($key); - $data[$key] = $value; - } - return $this->addValues($data, $expire); - } - - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * Nothing will be done if the cache already contains the key. - * @param mixed $key a key identifying the value to be cached. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @param mixed $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @param Dependency $dependency dependency of the cached item. If the dependency changes, - * the corresponding value in the cache will be invalidated when it is fetched via [[get()]]. - * This parameter is ignored if [[serializer]] is false. - * @return boolean whether the value is successfully stored into cache - */ - public function add($key, $value, $expire = 0, $dependency = null) - { - if ($dependency !== null && $this->serializer !== false) { - $dependency->evaluateDependency($this); - } - if ($this->serializer === null) { - $value = serialize([$value, $dependency]); - } elseif ($this->serializer !== false) { - $value = call_user_func($this->serializer[0], [$value, $dependency]); - } - $key = $this->buildKey($key); - return $this->addValue($key, $value, $expire); - } - - /** - * Deletes a value with the specified key from cache - * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean if no error happens during deletion - */ - public function delete($key) - { - $key = $this->buildKey($key); - return $this->deleteValue($key); - } - - /** - * Deletes all values from cache. - * Be careful of performing this operation if the cache is shared among multiple applications. - * @return boolean whether the flush operation was successful. - */ - public function flush() - { - return $this->flushValues(); - } - - /** - * Retrieves a value from cache with a specified key. - * This method should be implemented by child classes to retrieve the data - * from specific cache storage. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - abstract protected function getValue($key); - - /** - * Stores a value identified by a key in cache. - * This method should be implemented by child classes to store the data - * in specific cache storage. - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - abstract protected function setValue($key, $value, $expire); - - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This method should be implemented by child classes to store the data - * in specific cache storage. - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - abstract protected function addValue($key, $value, $expire); - - /** - * Deletes a value with the specified key from cache - * This method should be implemented by child classes to delete the data from actual cache storage. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - abstract protected function deleteValue($key); - - /** - * Deletes all values from cache. - * Child classes may implement this method to realize the flush operation. - * @return boolean whether the flush operation was successful. - */ - abstract protected function flushValues(); - - /** - * Retrieves multiple values from cache with the specified keys. - * The default implementation calls [[getValue()]] multiple times to retrieve - * the cached values one by one. If the underlying cache storage supports multiget, - * this method should be overridden to exploit that feature. - * @param array $keys a list of keys identifying the cached values - * @return array a list of cached values indexed by the keys - */ - protected function getValues($keys) - { - $results = []; - foreach ($keys as $key) { - $results[$key] = $this->getValue($key); - } - return $results; - } - - /** - * Stores multiple key-value pairs in cache. - * The default implementation calls [[setValue()]] multiple times store values one by one. If the underlying cache - * storage supports multi-set, this method should be overridden to exploit that feature. - * @param array $data array where key corresponds to cache key while value is the value stored - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys - */ - protected function setValues($data, $expire) - { - $failedKeys = []; - foreach ($data as $key => $value) { - if ($this->setValue($key, $value, $expire) === false) { - $failedKeys[] = $key; - } - } - return $failedKeys; - } - - /** - * Adds multiple key-value pairs to cache. - * The default implementation calls [[addValue()]] multiple times add values one by one. If the underlying cache - * storage supports multi-add, this method should be overridden to exploit that feature. - * @param array $data array where key corresponds to cache key while value is the value stored - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys - */ - protected function addValues($data, $expire) - { - $failedKeys = []; - foreach ($data as $key => $value) { - if ($this->addValue($key, $value, $expire) === false) { - $failedKeys[] = $key; - } - } - return $failedKeys; - } - - /** - * Returns whether there is a cache entry with a specified key. - * This method is required by the interface ArrayAccess. - * @param string $key a key identifying the cached value - * @return boolean - */ - public function offsetExists($key) - { - return $this->get($key) !== false; - } - - /** - * Retrieves the value from cache with a specified key. - * This method is required by the interface ArrayAccess. - * @param string $key a key identifying the cached value - * @return mixed the value stored in cache, false if the value is not in the cache or expired. - */ - public function offsetGet($key) - { - return $this->get($key); - } - - /** - * Stores the value identified by a key into cache. - * If the cache already contains such a key, the existing value will be - * replaced with the new ones. To add expiration and dependencies, use the [[set()]] method. - * This method is required by the interface ArrayAccess. - * @param string $key the key identifying the value to be cached - * @param mixed $value the value to be cached - */ - public function offsetSet($key, $value) - { - $this->set($key, $value); - } - - /** - * Deletes the value with the specified key from cache - * This method is required by the interface ArrayAccess. - * @param string $key the key of the value to be deleted - */ - public function offsetUnset($key) - { - $this->delete($key); - } + /** + * @var string a string prefixed to every cache key so that it is unique. If not set, + * it will use a prefix generated from [[\yii\base\Application::id]]. You may set this property to be an empty string + * if you don't want to use key prefix. It is recommended that you explicitly set this property to some + * static value if the cached data needs to be shared among multiple applications. + * + * To ensure interoperability, only alphanumeric characters should be used. + */ + public $keyPrefix; + /** + * @var array|boolean the functions used to serialize and unserialize cached data. Defaults to null, meaning + * using the default PHP `serialize()` and `unserialize()` functions. If you want to use some more efficient + * serializer (e.g. [igbinary](http://pecl.php.net/package/igbinary)), you may configure this property with + * a two-element array. The first element specifies the serialization function, and the second the deserialization + * function. If this property is set false, data will be directly sent to and retrieved from the underlying + * cache component without any serialization or deserialization. You should not turn off serialization if + * you are using [[Dependency|cache dependency]], because it relies on data serialization. + */ + public $serializer; + + /** + * Initializes the application component. + * This method overrides the parent implementation by setting default cache key prefix. + */ + public function init() + { + parent::init(); + if ($this->keyPrefix === null) { + $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); + } + } + + /** + * Builds a normalized cache key from a given key. + * + * If the given key is a string containing alphanumeric characters only and no more than 32 characters, + * then the key will be returned back prefixed with [[keyPrefix]]. Otherwise, a normalized key + * is generated by serializing the given key, applying MD5 hashing, and prefixing with [[keyPrefix]]. + * + * @param mixed $key the key to be normalized + * @return string the generated cache key + */ + protected function buildKey($key) + { + if (is_string($key)) { + $key = ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key); + } else { + $key = md5(json_encode($key)); + } + + return $this->keyPrefix . $key; + } + + /** + * Retrieves a value from cache with a specified key. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return mixed the value stored in cache, false if the value is not in the cache, expired, + * or the dependency associated with the cached data has changed. + */ + public function get($key) + { + $key = $this->buildKey($key); + $value = $this->getValue($key); + if ($value === false || $this->serializer === false) { + return $value; + } elseif ($this->serializer === null) { + $value = unserialize($value); + } else { + $value = call_user_func($this->serializer[1], $value); + } + if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged($this))) { + return $value[0]; + } else { + return false; + } + } + + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * In case a cache does not support this feature natively, this method will try to simulate it + * but has no performance improvement over getting it. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); + $value = $this->getValue($key); + + return $value !== false; + } + + /** + * Retrieves multiple values from cache with the specified keys. + * Some caches (such as memcache, apc) allow retrieving multiple cached values at the same time, + * which may improve the performance. In case a cache does not support this feature natively, + * this method will try to simulate it. + * @param array $keys list of keys identifying the cached values + * @return array list of cached values corresponding to the specified keys. The array + * is returned in terms of (key, value) pairs. + * If a value is not cached or expired, the corresponding array value will be false. + */ + public function mget($keys) + { + $keyMap = []; + foreach ($keys as $key) { + $keyMap[$key] = $this->buildKey($key); + } + $values = $this->getValues(array_values($keyMap)); + $results = []; + foreach ($keyMap as $key => $newKey) { + $results[$key] = false; + if (isset($values[$newKey])) { + if ($this->serializer === false) { + $results[$key] = $values[$newKey]; + } else { + $value = $this->serializer === null ? unserialize($values[$newKey]) + : call_user_func($this->serializer[1], $values[$newKey]); + + if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->getHasChanged($this))) { + $results[$key] = $value[0]; + } + } + } + } + + return $results; + } + + /** + * Stores a value identified by a key into cache. + * If the cache already contains such a key, the existing value and + * expiration time will be replaced with the new ones, respectively. + * + * @param mixed $key a key identifying the value to be cached. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @param mixed $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @param Dependency $dependency dependency of the cached item. If the dependency changes, + * the corresponding value in the cache will be invalidated when it is fetched via [[get()]]. + * This parameter is ignored if [[serializer]] is false. + * @return boolean whether the value is successfully stored into cache + */ + public function set($key, $value, $expire = 0, $dependency = null) + { + if ($dependency !== null && $this->serializer !== false) { + $dependency->evaluateDependency($this); + } + if ($this->serializer === null) { + $value = serialize([$value, $dependency]); + } elseif ($this->serializer !== false) { + $value = call_user_func($this->serializer[0], [$value, $dependency]); + } + $key = $this->buildKey($key); + + return $this->setValue($key, $value, $expire); + } + + /** + * Stores multiple items in cache. Each item contains a value identified by a key. + * If the cache already contains such a key, the existing value and + * expiration time will be replaced with the new ones, respectively. + * + * @param array $items the items to be cached, as key-value pairs. + * @param integer $expire default number of seconds in which the cached values will expire. 0 means never expire. + * @param Dependency $dependency dependency of the cached items. If the dependency changes, + * the corresponding values in the cache will be invalidated when it is fetched via [[get()]]. + * This parameter is ignored if [[serializer]] is false. + * @return boolean whether the items are successfully stored into cache + */ + public function mset($items, $expire = 0, $dependency = null) + { + if ($dependency !== null && $this->serializer !== false) { + $dependency->evaluateDependency($this); + } + + $data = []; + foreach ($items as $key => $value) { + if ($this->serializer === null) { + $value = serialize([$value, $dependency]); + } elseif ($this->serializer !== false) { + $value = call_user_func($this->serializer[0], [$value, $dependency]); + } + + $key = $this->buildKey($key); + $data[$key] = $value; + } + + return $this->setValues($data, $expire); + } + + /** + * Stores multiple items in cache. Each item contains a value identified by a key. + * If the cache already contains such a key, the existing value and expiration time will be preserved. + * + * @param array $items the items to be cached, as key-value pairs. + * @param integer $expire default number of seconds in which the cached values will expire. 0 means never expire. + * @param Dependency $dependency dependency of the cached items. If the dependency changes, + * the corresponding values in the cache will be invalidated when it is fetched via [[get()]]. + * This parameter is ignored if [[serializer]] is false. + * @return boolean whether the items are successfully stored into cache + */ + public function madd($items, $expire = 0, $dependency = null) + { + if ($dependency !== null && $this->serializer !== false) { + $dependency->evaluateDependency($this); + } + + $data = []; + foreach ($items as $key => $value) { + if ($this->serializer === null) { + $value = serialize([$value, $dependency]); + } elseif ($this->serializer !== false) { + $value = call_user_func($this->serializer[0], [$value, $dependency]); + } + + $key = $this->buildKey($key); + $data[$key] = $value; + } + + return $this->addValues($data, $expire); + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * Nothing will be done if the cache already contains the key. + * @param mixed $key a key identifying the value to be cached. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @param mixed $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @param Dependency $dependency dependency of the cached item. If the dependency changes, + * the corresponding value in the cache will be invalidated when it is fetched via [[get()]]. + * This parameter is ignored if [[serializer]] is false. + * @return boolean whether the value is successfully stored into cache + */ + public function add($key, $value, $expire = 0, $dependency = null) + { + if ($dependency !== null && $this->serializer !== false) { + $dependency->evaluateDependency($this); + } + if ($this->serializer === null) { + $value = serialize([$value, $dependency]); + } elseif ($this->serializer !== false) { + $value = call_user_func($this->serializer[0], [$value, $dependency]); + } + $key = $this->buildKey($key); + + return $this->addValue($key, $value, $expire); + } + + /** + * Deletes a value with the specified key from cache + * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean if no error happens during deletion + */ + public function delete($key) + { + $key = $this->buildKey($key); + + return $this->deleteValue($key); + } + + /** + * Deletes all values from cache. + * Be careful of performing this operation if the cache is shared among multiple applications. + * @return boolean whether the flush operation was successful. + */ + public function flush() + { + return $this->flushValues(); + } + + /** + * Retrieves a value from cache with a specified key. + * This method should be implemented by child classes to retrieve the data + * from specific cache storage. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + abstract protected function getValue($key); + + /** + * Stores a value identified by a key in cache. + * This method should be implemented by child classes to store the data + * in specific cache storage. + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + abstract protected function setValue($key, $value, $expire); + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This method should be implemented by child classes to store the data + * in specific cache storage. + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + abstract protected function addValue($key, $value, $expire); + + /** + * Deletes a value with the specified key from cache + * This method should be implemented by child classes to delete the data from actual cache storage. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + abstract protected function deleteValue($key); + + /** + * Deletes all values from cache. + * Child classes may implement this method to realize the flush operation. + * @return boolean whether the flush operation was successful. + */ + abstract protected function flushValues(); + + /** + * Retrieves multiple values from cache with the specified keys. + * The default implementation calls [[getValue()]] multiple times to retrieve + * the cached values one by one. If the underlying cache storage supports multiget, + * this method should be overridden to exploit that feature. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->getValue($key); + } + + return $results; + } + + /** + * Stores multiple key-value pairs in cache. + * The default implementation calls [[setValue()]] multiple times store values one by one. If the underlying cache + * storage supports multi-set, this method should be overridden to exploit that feature. + * @param array $data array where key corresponds to cache key while value is the value stored + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys + */ + protected function setValues($data, $expire) + { + $failedKeys = []; + foreach ($data as $key => $value) { + if ($this->setValue($key, $value, $expire) === false) { + $failedKeys[] = $key; + } + } + + return $failedKeys; + } + + /** + * Adds multiple key-value pairs to cache. + * The default implementation calls [[addValue()]] multiple times add values one by one. If the underlying cache + * storage supports multi-add, this method should be overridden to exploit that feature. + * @param array $data array where key corresponds to cache key while value is the value stored + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys + */ + protected function addValues($data, $expire) + { + $failedKeys = []; + foreach ($data as $key => $value) { + if ($this->addValue($key, $value, $expire) === false) { + $failedKeys[] = $key; + } + } + + return $failedKeys; + } + + /** + * Returns whether there is a cache entry with a specified key. + * This method is required by the interface ArrayAccess. + * @param string $key a key identifying the cached value + * @return boolean + */ + public function offsetExists($key) + { + return $this->get($key) !== false; + } + + /** + * Retrieves the value from cache with a specified key. + * This method is required by the interface ArrayAccess. + * @param string $key a key identifying the cached value + * @return mixed the value stored in cache, false if the value is not in the cache or expired. + */ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Stores the value identified by a key into cache. + * If the cache already contains such a key, the existing value will be + * replaced with the new ones. To add expiration and dependencies, use the [[set()]] method. + * This method is required by the interface ArrayAccess. + * @param string $key the key identifying the value to be cached + * @param mixed $value the value to be cached + */ + public function offsetSet($key, $value) + { + $this->set($key, $value); + } + + /** + * Deletes the value with the specified key from cache + * This method is required by the interface ArrayAccess. + * @param string $key the key of the value to be deleted + */ + public function offsetUnset($key) + { + $this->delete($key); + } } diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index f8bfee3c21e..46e014e0ac1 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -19,57 +19,58 @@ */ class ChainedDependency extends Dependency { - /** - * @var Dependency[] list of dependencies that this dependency is composed of. - * Each array element must be a dependency object. - */ - public $dependencies = []; - /** - * @var boolean whether this dependency is depending on every dependency in [[dependencies]]. - * Defaults to true, meaning if any of the dependencies has changed, this dependency is considered changed. - * When it is set false, it means if one of the dependencies has NOT changed, this dependency - * is considered NOT changed. - */ - public $dependOnAll = true; + /** + * @var Dependency[] list of dependencies that this dependency is composed of. + * Each array element must be a dependency object. + */ + public $dependencies = []; + /** + * @var boolean whether this dependency is depending on every dependency in [[dependencies]]. + * Defaults to true, meaning if any of the dependencies has changed, this dependency is considered changed. + * When it is set false, it means if one of the dependencies has NOT changed, this dependency + * is considered NOT changed. + */ + public $dependOnAll = true; - /** - * Evaluates the dependency by generating and saving the data related with dependency. - * @param Cache $cache the cache component that is currently evaluating this dependency - */ - public function evaluateDependency($cache) - { - foreach ($this->dependencies as $dependency) { - $dependency->evaluateDependency($cache); - } - } + /** + * Evaluates the dependency by generating and saving the data related with dependency. + * @param Cache $cache the cache component that is currently evaluating this dependency + */ + public function evaluateDependency($cache) + { + foreach ($this->dependencies as $dependency) { + $dependency->evaluateDependency($cache); + } + } - /** - * Generates the data needed to determine if dependency has been changed. - * This method does nothing in this class. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return mixed the data needed to determine if dependency has been changed. - */ - protected function generateDependencyData($cache) - { - return null; - } + /** + * Generates the data needed to determine if dependency has been changed. + * This method does nothing in this class. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + */ + protected function generateDependencyData($cache) + { + return null; + } - /** - * Performs the actual dependency checking. - * This method returns true if any of the dependency objects - * reports a dependency change. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return boolean whether the dependency is changed or not. - */ - public function getHasChanged($cache) - { - foreach ($this->dependencies as $dependency) { - if ($this->dependOnAll && $dependency->getHasChanged($cache)) { - return true; - } elseif (!$this->dependOnAll && !$dependency->getHasChanged($cache)) { - return false; - } - } - return !$this->dependOnAll; - } + /** + * Performs the actual dependency checking. + * This method returns true if any of the dependency objects + * reports a dependency change. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return boolean whether the dependency is changed or not. + */ + public function getHasChanged($cache) + { + foreach ($this->dependencies as $dependency) { + if ($this->dependOnAll && $dependency->getHasChanged($cache)) { + return true; + } elseif (!$this->dependOnAll && !$dependency->getHasChanged($cache)) { + return false; + } + } + + return !$this->dependOnAll; + } } diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 76e2341792f..0731b1eddaa 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -35,240 +35,246 @@ */ class DbCache extends Cache { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbCache object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; - /** - * @var string name of the DB table to store cache content. - * The table should be pre-created as follows: - * - * ~~~ - * CREATE TABLE tbl_cache ( - * id char(128) NOT NULL PRIMARY KEY, - * expire int(11), - * data BLOB - * ); - * ~~~ - * - * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type - * that can be used for some popular DBMS: - * - * - MySQL: LONGBLOB - * - PostgreSQL: BYTEA - * - MSSQL: BLOB - * - * When using DbCache in a production server, we recommend you create a DB index for the 'expire' - * column in the cache table to improve the performance. - */ - public $cacheTable = '{{%cache}}'; - /** - * @var integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. - * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. - **/ - public $gcProbability = 100; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbCache object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_cache ( + * id char(128) NOT NULL PRIMARY KEY, + * expire int(11), + * data BLOB + * ); + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbCache in a production server, we recommend you create a DB index for the 'expire' + * column in the cache table to improve the performance. + */ + public $cacheTable = '{{%cache}}'; + /** + * @var integer the probability (parts per million) that garbage collection (GC) should be performed + * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. + * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. + **/ + public $gcProbability = 100; + /** + * Initializes the DbCache component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbCache::db must be either a DB connection instance or the application component ID of a DB connection."); + } + } - /** - * Initializes the DbCache component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbCache::db must be either a DB connection instance or the application component ID of a DB connection."); - } - } + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - $key = $this->buildKey($key); + $query = new Query; + $query->select(['COUNT(*)']) + ->from($this->cacheTable) + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', [':id' => $key]); + if ($this->db->enableQueryCache) { + // temporarily disable and re-enable query caching + $this->db->enableQueryCache = false; + $result = $query->createCommand($this->db)->queryScalar(); + $this->db->enableQueryCache = true; + } else { + $result = $query->createCommand($this->db)->queryScalar(); + } - $query = new Query; - $query->select(['COUNT(*)']) - ->from($this->cacheTable) - ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', [':id' => $key]); - if ($this->db->enableQueryCache) { - // temporarily disable and re-enable query caching - $this->db->enableQueryCache = false; - $result = $query->createCommand($this->db)->queryScalar(); - $this->db->enableQueryCache = true; - } else { - $result = $query->createCommand($this->db)->queryScalar(); - } - return $result > 0; - } + return $result > 0; + } - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - $query = new Query; - $query->select(['data']) - ->from($this->cacheTable) - ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', [':id' => $key]); - if ($this->db->enableQueryCache) { - // temporarily disable and re-enable query caching - $this->db->enableQueryCache = false; - $result = $query->createCommand($this->db)->queryScalar(); - $this->db->enableQueryCache = true; - return $result; - } else { - return $query->createCommand($this->db)->queryScalar(); - } - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + $query = new Query; + $query->select(['data']) + ->from($this->cacheTable) + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', [':id' => $key]); + if ($this->db->enableQueryCache) { + // temporarily disable and re-enable query caching + $this->db->enableQueryCache = false; + $result = $query->createCommand($this->db)->queryScalar(); + $this->db->enableQueryCache = true; - /** - * Retrieves multiple values from cache with the specified keys. - * @param array $keys a list of keys identifying the cached values - * @return array a list of cached values indexed by the keys - */ - protected function getValues($keys) - { - if (empty($keys)) { - return []; - } - $query = new Query; - $query->select(['id', 'data']) - ->from($this->cacheTable) - ->where(['id' => $keys]) - ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); + return $result; + } else { + return $query->createCommand($this->db)->queryScalar(); + } + } - if ($this->db->enableQueryCache) { - $this->db->enableQueryCache = false; - $rows = $query->createCommand($this->db)->queryAll(); - $this->db->enableQueryCache = true; - } else { - $rows = $query->createCommand($this->db)->queryAll(); - } + /** + * Retrieves multiple values from cache with the specified keys. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + if (empty($keys)) { + return []; + } + $query = new Query; + $query->select(['id', 'data']) + ->from($this->cacheTable) + ->where(['id' => $keys]) + ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); - $results = []; - foreach ($keys as $key) { - $results[$key] = false; - } - foreach ($rows as $row) { - $results[$row['id']] = $row['data']; - } - return $results; - } + if ($this->db->enableQueryCache) { + $this->db->enableQueryCache = false; + $rows = $query->createCommand($this->db)->queryAll(); + $this->db->enableQueryCache = true; + } else { + $rows = $query->createCommand($this->db)->queryAll(); + } - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - $command = $this->db->createCommand() - ->update($this->cacheTable, [ - 'expire' => $expire > 0 ? $expire + time() : 0, - 'data' => [$value, \PDO::PARAM_LOB], - ], ['id' => $key]); + $results = []; + foreach ($keys as $key) { + $results[$key] = false; + } + foreach ($rows as $row) { + $results[$row['id']] = $row['data']; + } - if ($command->execute()) { - $this->gc(); - return true; - } else { - return $this->addValue($key, $value, $expire); - } - } + return $results; + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - $this->gc(); + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + $command = $this->db->createCommand() + ->update($this->cacheTable, [ + 'expire' => $expire > 0 ? $expire + time() : 0, + 'data' => [$value, \PDO::PARAM_LOB], + ], ['id' => $key]); - if ($expire > 0) { - $expire += time(); - } else { - $expire = 0; - } + if ($command->execute()) { + $this->gc(); - try { - $this->db->createCommand() - ->insert($this->cacheTable, [ - 'id' => $key, - 'expire' => $expire, - 'data' => [$value, \PDO::PARAM_LOB], - ])->execute(); - return true; - } catch (\Exception $e) { - return false; - } - } + return true; + } else { + return $this->addValue($key, $value, $expire); + } + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - $this->db->createCommand() - ->delete($this->cacheTable, ['id' => $key]) - ->execute(); - return true; - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + $this->gc(); - /** - * Removes the expired data values. - * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. - * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. - */ - public function gc($force = false) - { - if ($force || mt_rand(0, 1000000) < $this->gcProbability) { - $this->db->createCommand() - ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time()) - ->execute(); - } - } + if ($expire > 0) { + $expire += time(); + } else { + $expire = 0; + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - $this->db->createCommand() - ->delete($this->cacheTable) - ->execute(); - return true; - } + try { + $this->db->createCommand() + ->insert($this->cacheTable, [ + 'id' => $key, + 'expire' => $expire, + 'data' => [$value, \PDO::PARAM_LOB], + ])->execute(); + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + $this->db->createCommand() + ->delete($this->cacheTable, ['id' => $key]) + ->execute(); + + return true; + } + + /** + * Removes the expired data values. + * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. + * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. + */ + public function gc($force = false) + { + if ($force || mt_rand(0, 1000000) < $this->gcProbability) { + $this->db->createCommand() + ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time()) + ->execute(); + } + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + $this->db->createCommand() + ->delete($this->cacheTable) + ->execute(); + + return true; + } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index ac6f2e7d1e2..a705d89ed80 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -22,45 +22,46 @@ */ class DbDependency extends Dependency { - /** - * @var string the application component ID of the DB connection. - */ - public $db = 'db'; - /** - * @var string the SQL query whose result is used to determine if the dependency has been changed. - * Only the first row of the query result will be used. - */ - public $sql; - /** - * @var array the parameters (name => value) to be bound to the SQL statement specified by [[sql]]. - */ - public $params = []; + /** + * @var string the application component ID of the DB connection. + */ + public $db = 'db'; + /** + * @var string the SQL query whose result is used to determine if the dependency has been changed. + * Only the first row of the query result will be used. + */ + public $sql; + /** + * @var array the parameters (name => value) to be bound to the SQL statement specified by [[sql]]. + */ + public $params = []; - /** - * Generates the data needed to determine if dependency has been changed. - * This method returns the value of the global state. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return mixed the data needed to determine if dependency has been changed. - * @throws InvalidConfigException if [[db]] is not a valid application component ID - */ - protected function generateDependencyData($cache) - { - $db = Yii::$app->getComponent($this->db); - if (!$db instanceof Connection) { - throw new InvalidConfigException("DbDependency::db must be the application component ID of a DB connection."); - } - if ($this->sql === null) { - throw new InvalidConfigException("DbDependency::sql must be set."); - } + /** + * Generates the data needed to determine if dependency has been changed. + * This method returns the value of the global state. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + * @throws InvalidConfigException if [[db]] is not a valid application component ID + */ + protected function generateDependencyData($cache) + { + $db = Yii::$app->getComponent($this->db); + if (!$db instanceof Connection) { + throw new InvalidConfigException("DbDependency::db must be the application component ID of a DB connection."); + } + if ($this->sql === null) { + throw new InvalidConfigException("DbDependency::sql must be set."); + } - if ($db->enableQueryCache) { - // temporarily disable and re-enable query caching - $db->enableQueryCache = false; - $result = $db->createCommand($this->sql, $this->params)->queryOne(); - $db->enableQueryCache = true; - } else { - $result = $db->createCommand($this->sql, $this->params)->queryOne(); - } - return $result; - } + if ($db->enableQueryCache) { + // temporarily disable and re-enable query caching + $db->enableQueryCache = false; + $result = $db->createCommand($this->sql, $this->params)->queryOne(); + $db->enableQueryCache = true; + } else { + $result = $db->createCommand($this->sql, $this->params)->queryOne(); + } + + return $result; + } } diff --git a/framework/caching/Dependency.php b/framework/caching/Dependency.php index c84f72ddf5a..9946955d31c 100644 --- a/framework/caching/Dependency.php +++ b/framework/caching/Dependency.php @@ -18,82 +18,82 @@ */ abstract class Dependency extends \yii\base\Object { - /** - * @var mixed the dependency data that is saved in cache and later is compared with the - * latest dependency data. - */ - public $data; - /** - * @var boolean whether this dependency is reusable or not. True value means that dependent - * data for this cache dependency will be generated only once per request. This allows you - * to use the same cache dependency for multiple separate cache calls while generating the same - * page without an overhead of re-evaluating dependency data each time. Defaults to false. - */ - public $reusable = false; + /** + * @var mixed the dependency data that is saved in cache and later is compared with the + * latest dependency data. + */ + public $data; + /** + * @var boolean whether this dependency is reusable or not. True value means that dependent + * data for this cache dependency will be generated only once per request. This allows you + * to use the same cache dependency for multiple separate cache calls while generating the same + * page without an overhead of re-evaluating dependency data each time. Defaults to false. + */ + public $reusable = false; - /** - * @var array static storage of cached data for reusable dependencies. - */ - private static $_reusableData = []; - /** - * @var string a unique hash value for this cache dependency. - */ - private $_hash; + /** + * @var array static storage of cached data for reusable dependencies. + */ + private static $_reusableData = []; + /** + * @var string a unique hash value for this cache dependency. + */ + private $_hash; + /** + * Evaluates the dependency by generating and saving the data related with dependency. + * This method is invoked by cache before writing data into it. + * @param Cache $cache the cache component that is currently evaluating this dependency + */ + public function evaluateDependency($cache) + { + if (!$this->reusable) { + $this->data = $this->generateDependencyData($cache); + } else { + if ($this->_hash === null) { + $this->_hash = sha1(serialize($this)); + } + if (!array_key_exists($this->_hash, self::$_reusableData)) { + self::$_reusableData[$this->_hash] = $this->generateDependencyData($cache); + } + $this->data = self::$_reusableData[$this->_hash]; + } + } - /** - * Evaluates the dependency by generating and saving the data related with dependency. - * This method is invoked by cache before writing data into it. - * @param Cache $cache the cache component that is currently evaluating this dependency - */ - public function evaluateDependency($cache) - { - if (!$this->reusable) { - $this->data = $this->generateDependencyData($cache); - } else { - if ($this->_hash === null) { - $this->_hash = sha1(serialize($this)); - } - if (!array_key_exists($this->_hash, self::$_reusableData)) { - self::$_reusableData[$this->_hash] = $this->generateDependencyData($cache); - } - $this->data = self::$_reusableData[$this->_hash]; - } - } + /** + * Returns a value indicating whether the dependency has changed. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return boolean whether the dependency has changed. + */ + public function getHasChanged($cache) + { + if (!$this->reusable) { + return $this->generateDependencyData($cache) !== $this->data; + } else { + if ($this->_hash === null) { + $this->_hash = sha1(serialize($this)); + } + if (!array_key_exists($this->_hash, self::$_reusableData)) { + self::$_reusableData[$this->_hash] = $this->generateDependencyData($cache); + } - /** - * Returns a value indicating whether the dependency has changed. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return boolean whether the dependency has changed. - */ - public function getHasChanged($cache) - { - if (!$this->reusable) { - return $this->generateDependencyData($cache) !== $this->data; - } else { - if ($this->_hash === null) { - $this->_hash = sha1(serialize($this)); - } - if (!array_key_exists($this->_hash, self::$_reusableData)) { - self::$_reusableData[$this->_hash] = $this->generateDependencyData($cache); - } - return self::$_reusableData[$this->_hash] !== $this->data; - } - } + return self::$_reusableData[$this->_hash] !== $this->data; + } + } - /** - * Resets all cached data for reusable dependencies. - */ - public static function resetReusableData() - { - self::$_reusableData = []; - } + /** + * Resets all cached data for reusable dependencies. + */ + public static function resetReusableData() + { + self::$_reusableData = []; + } - /** - * Generates the data needed to determine if dependency has been changed. - * Derived classes should override this method to generate the actual dependency data. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return mixed the data needed to determine if dependency has been changed. - */ - abstract protected function generateDependencyData($cache); + /** + * Generates the data needed to determine if dependency has been changed. + * Derived classes should override this method to generate the actual dependency data. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + */ + abstract protected function generateDependencyData($cache); } diff --git a/framework/caching/DummyCache.php b/framework/caching/DummyCache.php index 8d900dfedc0..5757874462d 100644 --- a/framework/caching/DummyCache.php +++ b/framework/caching/DummyCache.php @@ -20,62 +20,62 @@ */ class DummyCache extends Cache { - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - return false; - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return false; + } - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - return true; - } + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + return true; + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - return true; - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + return true; + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - return true; - } + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return true; + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - return true; - } + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + return true; + } } diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index f6642b403c8..e886483bc2e 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -22,26 +22,26 @@ */ class ExpressionDependency extends Dependency { - /** - * @var string the string representation of a PHP expression whose result is used to determine the dependency. - * A PHP expression can be any PHP code that evaluates to a value. To learn more about what an expression is, - * please refer to the [php manual](http://www.php.net/manual/en/language.expressions.php). - */ - public $expression = 'true'; - /** - * @var mixed custom parameters associated with this dependency. You may get the value - * of this property in [[expression]] using `$this->params`. - */ - public $params; + /** + * @var string the string representation of a PHP expression whose result is used to determine the dependency. + * A PHP expression can be any PHP code that evaluates to a value. To learn more about what an expression is, + * please refer to the [php manual](http://www.php.net/manual/en/language.expressions.php). + */ + public $expression = 'true'; + /** + * @var mixed custom parameters associated with this dependency. You may get the value + * of this property in [[expression]] using `$this->params`. + */ + public $params; - /** - * Generates the data needed to determine if dependency has been changed. - * This method returns the result of the PHP expression. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return mixed the data needed to determine if dependency has been changed. - */ - protected function generateDependencyData($cache) - { - return eval("return {$this->expression};"); - } + /** + * Generates the data needed to determine if dependency has been changed. + * This method returns the result of the PHP expression. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + */ + protected function generateDependencyData($cache) + { + return eval("return {$this->expression};"); + } } diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index 14efa6888e2..e9e65105522 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -24,225 +24,230 @@ */ class FileCache extends Cache { - /** - * @var string a string prefixed to every cache key. This is needed when you store - * cache data under the same [[cachePath]] for different applications to avoid - * conflict. - * - * To ensure interoperability, only alphanumeric characters should be used. - */ - public $keyPrefix = ''; - /** - * @var string the directory to store cache files. You may use path alias here. - * If not set, it will use the "cache" subdirectory under the application runtime path. - */ - public $cachePath = '@runtime/cache'; - /** - * @var string cache file suffix. Defaults to '.bin'. - */ - public $cacheFileSuffix = '.bin'; - /** - * @var integer the level of sub-directories to store cache files. Defaults to 1. - * If the system has huge number of cache files (e.g. one million), you may use a bigger value - * (usually no bigger than 3). Using sub-directories is mainly to ensure the file system - * is not over burdened with a single directory having too many files. - */ - public $directoryLevel = 1; - /** - * @var integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 10, meaning 0.001% chance. - * This number should be between 0 and 1000000. A value 0 means no GC will be performed at all. - **/ - public $gcProbability = 10; - /** - * @var integer the permission to be set for newly created cache files. - * This value will be used by PHP chmod() function. No umask will be applied. - * If not set, the permission will be determined by the current environment. - */ - public $fileMode; - /** - * @var integer the permission to be set for newly created directories. - * This value will be used by PHP chmod() function. No umask will be applied. - * Defaults to 0775, meaning the directory is read-writable by owner and group, - * but read-only for other users. - */ - public $dirMode = 0775; - - - /** - * Initializes this component by ensuring the existence of the cache path. - */ - public function init() - { - parent::init(); - $this->cachePath = Yii::getAlias($this->cachePath); - if (!is_dir($this->cachePath)) { - FileHelper::createDirectory($this->cachePath, $this->dirMode, true); - } - } - - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - $cacheFile = $this->getCacheFile($this->buildKey($key)); - return @filemtime($cacheFile) > time(); - } - - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - $cacheFile = $this->getCacheFile($key); - if (@filemtime($cacheFile) > time()) { - return @file_get_contents($cacheFile); - } else { - return false; - } - } - - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - if ($expire <= 0) { - $expire = 31536000; // 1 year - } - $expire += time(); - - $cacheFile = $this->getCacheFile($key); - if ($this->directoryLevel > 0) { - @FileHelper::createDirectory(dirname($cacheFile), $this->dirMode, true); - } - if (@file_put_contents($cacheFile, $value, LOCK_EX) !== false) { - if ($this->fileMode !== null) { - @chmod($cacheFile, $this->fileMode); - } - return @touch($cacheFile, $expire); - } else { - return false; - } - } - - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - $cacheFile = $this->getCacheFile($key); - if (@filemtime($cacheFile) > time()) { - return false; - } - return $this->setValue($key, $value, $expire); - } - - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - $cacheFile = $this->getCacheFile($key); - return @unlink($cacheFile); - } - - /** - * Returns the cache file path given the cache key. - * @param string $key cache key - * @return string the cache file path - */ - protected function getCacheFile($key) - { - if ($this->directoryLevel > 0) { - $base = $this->cachePath; - for ($i = 0; $i < $this->directoryLevel; ++$i) { - if (($prefix = substr($key, $i + $i, 2)) !== false) { - $base .= DIRECTORY_SEPARATOR . $prefix; - } - } - return $base . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; - } else { - return $this->cachePath . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; - } - } - - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - $this->gc(true, false); - return true; - } - - /** - * Removes expired cache files. - * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. - * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. - * @param boolean $expiredOnly whether to removed expired cache files only. - * If true, all cache files under [[cachePath]] will be removed. - */ - public function gc($force = false, $expiredOnly = true) - { - if ($force || mt_rand(0, 1000000) < $this->gcProbability) { - $this->gcRecursive($this->cachePath, $expiredOnly); - } - } - - /** - * Recursively removing expired cache files under a directory. - * This method is mainly used by [[gc()]]. - * @param string $path the directory under which expired cache files are removed. - * @param boolean $expiredOnly whether to only remove expired cache files. If false, all files - * under `$path` will be removed. - */ - protected function gcRecursive($path, $expiredOnly) - { - if (($handle = opendir($path)) !== false) { - while (($file = readdir($handle)) !== false) { - if ($file[0] === '.') { - continue; - } - $fullPath = $path . DIRECTORY_SEPARATOR . $file; - if (is_dir($fullPath)) { - $this->gcRecursive($fullPath, $expiredOnly); - if (!$expiredOnly) { - @rmdir($fullPath); - } - } elseif (!$expiredOnly || $expiredOnly && @filemtime($fullPath) < time()) { - @unlink($fullPath); - } - } - closedir($handle); - } - } + /** + * @var string a string prefixed to every cache key. This is needed when you store + * cache data under the same [[cachePath]] for different applications to avoid + * conflict. + * + * To ensure interoperability, only alphanumeric characters should be used. + */ + public $keyPrefix = ''; + /** + * @var string the directory to store cache files. You may use path alias here. + * If not set, it will use the "cache" subdirectory under the application runtime path. + */ + public $cachePath = '@runtime/cache'; + /** + * @var string cache file suffix. Defaults to '.bin'. + */ + public $cacheFileSuffix = '.bin'; + /** + * @var integer the level of sub-directories to store cache files. Defaults to 1. + * If the system has huge number of cache files (e.g. one million), you may use a bigger value + * (usually no bigger than 3). Using sub-directories is mainly to ensure the file system + * is not over burdened with a single directory having too many files. + */ + public $directoryLevel = 1; + /** + * @var integer the probability (parts per million) that garbage collection (GC) should be performed + * when storing a piece of data in the cache. Defaults to 10, meaning 0.001% chance. + * This number should be between 0 and 1000000. A value 0 means no GC will be performed at all. + **/ + public $gcProbability = 10; + /** + * @var integer the permission to be set for newly created cache files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + + /** + * Initializes this component by ensuring the existence of the cache path. + */ + public function init() + { + parent::init(); + $this->cachePath = Yii::getAlias($this->cachePath); + if (!is_dir($this->cachePath)) { + FileHelper::createDirectory($this->cachePath, $this->dirMode, true); + } + } + + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $cacheFile = $this->getCacheFile($this->buildKey($key)); + + return @filemtime($cacheFile) > time(); + } + + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + $cacheFile = $this->getCacheFile($key); + if (@filemtime($cacheFile) > time()) { + return @file_get_contents($cacheFile); + } else { + return false; + } + } + + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + if ($expire <= 0) { + $expire = 31536000; // 1 year + } + $expire += time(); + + $cacheFile = $this->getCacheFile($key); + if ($this->directoryLevel > 0) { + @FileHelper::createDirectory(dirname($cacheFile), $this->dirMode, true); + } + if (@file_put_contents($cacheFile, $value, LOCK_EX) !== false) { + if ($this->fileMode !== null) { + @chmod($cacheFile, $this->fileMode); + } + + return @touch($cacheFile, $expire); + } else { + return false; + } + } + + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + $cacheFile = $this->getCacheFile($key); + if (@filemtime($cacheFile) > time()) { + return false; + } + + return $this->setValue($key, $value, $expire); + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + $cacheFile = $this->getCacheFile($key); + + return @unlink($cacheFile); + } + + /** + * Returns the cache file path given the cache key. + * @param string $key cache key + * @return string the cache file path + */ + protected function getCacheFile($key) + { + if ($this->directoryLevel > 0) { + $base = $this->cachePath; + for ($i = 0; $i < $this->directoryLevel; ++$i) { + if (($prefix = substr($key, $i + $i, 2)) !== false) { + $base .= DIRECTORY_SEPARATOR . $prefix; + } + } + + return $base . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; + } else { + return $this->cachePath . DIRECTORY_SEPARATOR . $key . $this->cacheFileSuffix; + } + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + $this->gc(true, false); + + return true; + } + + /** + * Removes expired cache files. + * @param boolean $force whether to enforce the garbage collection regardless of [[gcProbability]]. + * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. + * @param boolean $expiredOnly whether to removed expired cache files only. + * If true, all cache files under [[cachePath]] will be removed. + */ + public function gc($force = false, $expiredOnly = true) + { + if ($force || mt_rand(0, 1000000) < $this->gcProbability) { + $this->gcRecursive($this->cachePath, $expiredOnly); + } + } + + /** + * Recursively removing expired cache files under a directory. + * This method is mainly used by [[gc()]]. + * @param string $path the directory under which expired cache files are removed. + * @param boolean $expiredOnly whether to only remove expired cache files. If false, all files + * under `$path` will be removed. + */ + protected function gcRecursive($path, $expiredOnly) + { + if (($handle = opendir($path)) !== false) { + while (($file = readdir($handle)) !== false) { + if ($file[0] === '.') { + continue; + } + $fullPath = $path . DIRECTORY_SEPARATOR . $file; + if (is_dir($fullPath)) { + $this->gcRecursive($fullPath, $expiredOnly); + if (!$expiredOnly) { + @rmdir($fullPath); + } + } elseif (!$expiredOnly || $expiredOnly && @filemtime($fullPath) < time()) { + @unlink($fullPath); + } + } + closedir($handle); + } + } } diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 200cdde9bbb..bd48e0a04a6 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -20,24 +20,25 @@ */ class FileDependency extends Dependency { - /** - * @var string the name of the file whose last modification time is used to - * check if the dependency has been changed. - */ - public $fileName; + /** + * @var string the name of the file whose last modification time is used to + * check if the dependency has been changed. + */ + public $fileName; - /** - * Generates the data needed to determine if dependency has been changed. - * This method returns the file's last modification time. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return mixed the data needed to determine if dependency has been changed. - * @throws InvalidConfigException if [[fileName]] is not set - */ - protected function generateDependencyData($cache) - { - if ($this->fileName === null) { - throw new InvalidConfigException('FileDependency::fileName must be set'); - } - return @filemtime($this->fileName); - } + /** + * Generates the data needed to determine if dependency has been changed. + * This method returns the file's last modification time. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + * @throws InvalidConfigException if [[fileName]] is not set + */ + protected function generateDependencyData($cache) + { + if ($this->fileName === null) { + throw new InvalidConfigException('FileDependency::fileName must be set'); + } + + return @filemtime($this->fileName); + } } diff --git a/framework/caching/GroupDependency.php b/framework/caching/GroupDependency.php index bcac858f27d..5129ac18cca 100644 --- a/framework/caching/GroupDependency.php +++ b/framework/caching/GroupDependency.php @@ -20,55 +20,58 @@ */ class GroupDependency extends Dependency { - /** - * @var string the group name. This property must be set. - */ - public $group; + /** + * @var string the group name. This property must be set. + */ + public $group; - /** - * Generates the data needed to determine if dependency has been changed. - * This method does nothing in this class. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return mixed the data needed to determine if dependency has been changed. - * @throws InvalidConfigException if [[group]] is not set. - */ - protected function generateDependencyData($cache) - { - if ($this->group === null) { - throw new InvalidConfigException('GroupDependency::group must be set'); - } - $version = $cache->get([__CLASS__, $this->group]); - if ($version === false) { - $version = $this->invalidate($cache, $this->group); - } - return $version; - } + /** + * Generates the data needed to determine if dependency has been changed. + * This method does nothing in this class. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + * @throws InvalidConfigException if [[group]] is not set. + */ + protected function generateDependencyData($cache) + { + if ($this->group === null) { + throw new InvalidConfigException('GroupDependency::group must be set'); + } + $version = $cache->get([__CLASS__, $this->group]); + if ($version === false) { + $version = $this->invalidate($cache, $this->group); + } - /** - * Performs the actual dependency checking. - * @param Cache $cache the cache component that is currently evaluating this dependency - * @return boolean whether the dependency is changed or not. - * @throws InvalidConfigException if [[group]] is not set. - */ - public function getHasChanged($cache) - { - if ($this->group === null) { - throw new InvalidConfigException('GroupDependency::group must be set'); - } - $version = $cache->get([__CLASS__, $this->group]); - return $version === false || $version !== $this->data; - } + return $version; + } - /** - * Invalidates all of the cached data items that have the same [[group]]. - * @param Cache $cache the cache component that caches the data items - * @param string $group the group name - * @return string the current version number - */ - public static function invalidate($cache, $group) - { - $version = microtime(); - $cache->set([__CLASS__, $group], $version); - return $version; - } + /** + * Performs the actual dependency checking. + * @param Cache $cache the cache component that is currently evaluating this dependency + * @return boolean whether the dependency is changed or not. + * @throws InvalidConfigException if [[group]] is not set. + */ + public function getHasChanged($cache) + { + if ($this->group === null) { + throw new InvalidConfigException('GroupDependency::group must be set'); + } + $version = $cache->get([__CLASS__, $this->group]); + + return $version === false || $version !== $this->data; + } + + /** + * Invalidates all of the cached data items that have the same [[group]]. + * @param Cache $cache the cache component that caches the data items + * @param string $group the group name + * @return string the current version number + */ + public static function invalidate($cache, $group) + { + $version = microtime(); + $cache->set([__CLASS__, $group], $version); + + return $version; + } } diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index 339179673a5..4f53fe4aba6 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -62,204 +62,206 @@ */ class MemCache extends Cache { - /** - * @var boolean whether to use memcached or memcache as the underlying caching extension. - * If true, [memcached](http://pecl.php.net/package/memcached) will be used. - * If false, [memcache](http://pecl.php.net/package/memcache) will be used. - * Defaults to false. - */ - public $useMemcached = false; - /** - * @var \Memcache|\Memcached the Memcache instance - */ - private $_cache = null; - /** - * @var array list of memcache server configurations - */ - private $_servers = []; + /** + * @var boolean whether to use memcached or memcache as the underlying caching extension. + * If true, [memcached](http://pecl.php.net/package/memcached) will be used. + * If false, [memcache](http://pecl.php.net/package/memcache) will be used. + * Defaults to false. + */ + public $useMemcached = false; + /** + * @var \Memcache|\Memcached the Memcache instance + */ + private $_cache = null; + /** + * @var array list of memcache server configurations + */ + private $_servers = []; - /** - * Initializes this application component. - * It creates the memcache instance and adds memcache servers. - */ - public function init() - { - parent::init(); - $servers = $this->getServers(); - $cache = $this->getMemCache(); - if (empty($servers)) { - $cache->addServer('127.0.0.1', 11211); - } else { - if (!$this->useMemcached) { - // different version of memcache may have different number of parameters for the addServer method. - $class = new \ReflectionClass($cache); - $paramCount = $class->getMethod('addServer')->getNumberOfParameters(); - } - foreach ($servers as $server) { - if ($server->host === null) { - throw new InvalidConfigException("The 'host' property must be specified for every memcache server."); - } - if ($this->useMemcached) { - $cache->addServer($server->host, $server->port, $server->weight); - } else { - // $timeout is used for memcache versions that do not have timeoutms parameter - $timeout = (int) ($server->timeout / 1000) + (($server->timeout % 1000 > 0) ? 1 : 0); - if ($paramCount === 9) { - $cache->addServer( - $server->host, $server->port, $server->persistent, - $server->weight, $timeout, $server->retryInterval, - $server->status, $server->failureCallback, $server->timeout - ); - } else { - $cache->addServer( - $server->host, $server->port, $server->persistent, - $server->weight, $timeout, $server->retryInterval, - $server->status, $server->failureCallback - ); - } - } - } - } - } + /** + * Initializes this application component. + * It creates the memcache instance and adds memcache servers. + */ + public function init() + { + parent::init(); + $servers = $this->getServers(); + $cache = $this->getMemCache(); + if (empty($servers)) { + $cache->addServer('127.0.0.1', 11211); + } else { + if (!$this->useMemcached) { + // different version of memcache may have different number of parameters for the addServer method. + $class = new \ReflectionClass($cache); + $paramCount = $class->getMethod('addServer')->getNumberOfParameters(); + } + foreach ($servers as $server) { + if ($server->host === null) { + throw new InvalidConfigException("The 'host' property must be specified for every memcache server."); + } + if ($this->useMemcached) { + $cache->addServer($server->host, $server->port, $server->weight); + } else { + // $timeout is used for memcache versions that do not have timeoutms parameter + $timeout = (int) ($server->timeout / 1000) + (($server->timeout % 1000 > 0) ? 1 : 0); + if ($paramCount === 9) { + $cache->addServer( + $server->host, $server->port, $server->persistent, + $server->weight, $timeout, $server->retryInterval, + $server->status, $server->failureCallback, $server->timeout + ); + } else { + $cache->addServer( + $server->host, $server->port, $server->persistent, + $server->weight, $timeout, $server->retryInterval, + $server->status, $server->failureCallback + ); + } + } + } + } + } - /** - * Returns the underlying memcache (or memcached) object. - * @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component. - * @throws InvalidConfigException if memcache or memcached extension is not loaded - */ - public function getMemcache() - { - if ($this->_cache === null) { - $extension = $this->useMemcached ? 'memcached' : 'memcache'; - if (!extension_loaded($extension)) { - throw new InvalidConfigException("MemCache requires PHP $extension extension to be loaded."); - } - $this->_cache = $this->useMemcached ? new \Memcached : new \Memcache; - } - return $this->_cache; - } + /** + * Returns the underlying memcache (or memcached) object. + * @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component. + * @throws InvalidConfigException if memcache or memcached extension is not loaded + */ + public function getMemcache() + { + if ($this->_cache === null) { + $extension = $this->useMemcached ? 'memcached' : 'memcache'; + if (!extension_loaded($extension)) { + throw new InvalidConfigException("MemCache requires PHP $extension extension to be loaded."); + } + $this->_cache = $this->useMemcached ? new \Memcached : new \Memcache; + } - /** - * Returns the memcache server configurations. - * @return MemCacheServer[] list of memcache server configurations. - */ - public function getServers() - { - return $this->_servers; - } + return $this->_cache; + } - /** - * @param array $config list of memcache server configurations. Each element must be an array - * with the following keys: host, port, persistent, weight, timeout, retryInterval, status. - * @see http://www.php.net/manual/en/function.Memcache-addServer.php - */ - public function setServers($config) - { - foreach ($config as $c) { - $this->_servers[] = new MemCacheServer($c); - } - } + /** + * Returns the memcache server configurations. + * @return MemCacheServer[] list of memcache server configurations. + */ + public function getServers() + { + return $this->_servers; + } - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - return $this->_cache->get($key); - } + /** + * @param array $config list of memcache server configurations. Each element must be an array + * with the following keys: host, port, persistent, weight, timeout, retryInterval, status. + * @see http://www.php.net/manual/en/function.Memcache-addServer.php + */ + public function setServers($config) + { + foreach ($config as $c) { + $this->_servers[] = new MemCacheServer($c); + } + } - /** - * Retrieves multiple values from cache with the specified keys. - * @param array $keys a list of keys identifying the cached values - * @return array a list of cached values indexed by the keys - */ - protected function getValues($keys) - { - return $this->useMemcached ? $this->_cache->getMulti($keys) : $this->_cache->get($keys); - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return $this->_cache->get($key); + } - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - if ($expire > 0) { - $expire += time(); - } else { - $expire = 0; - } + /** + * Retrieves multiple values from cache with the specified keys. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + return $this->useMemcached ? $this->_cache->getMulti($keys) : $this->_cache->get($keys); + } - return $this->useMemcached ? $this->_cache->set($key, $value, $expire) : $this->_cache->set($key, $value, 0, $expire); - } + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + if ($expire > 0) { + $expire += time(); + } else { + $expire = 0; + } - /** - * Stores multiple key-value pairs in cache. - * @param array $data array where key corresponds to cache key while value is the value stored - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys. Always empty in case of using memcached. - */ - protected function setValues($data, $expire) - { - if ($this->useMemcached) { - if ($expire > 0) { - $expire += time(); - } else { - $expire = 0; - } - $this->_cache->setMulti($data, $expire); - return []; - } else { - return parent::setValues($data, $expire); - } - } + return $this->useMemcached ? $this->_cache->set($key, $value, $expire) : $this->_cache->set($key, $value, 0, $expire); + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - if ($expire > 0) { - $expire += time(); - } else { - $expire = 0; - } + /** + * Stores multiple key-value pairs in cache. + * @param array $data array where key corresponds to cache key while value is the value stored + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys. Always empty in case of using memcached. + */ + protected function setValues($data, $expire) + { + if ($this->useMemcached) { + if ($expire > 0) { + $expire += time(); + } else { + $expire = 0; + } + $this->_cache->setMulti($data, $expire); - return $this->useMemcached ? $this->_cache->add($key, $value, $expire) : $this->_cache->add($key, $value, 0, $expire); - } + return []; + } else { + return parent::setValues($data, $expire); + } + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - return $this->_cache->delete($key, 0); - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + if ($expire > 0) { + $expire += time(); + } else { + $expire = 0; + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - return $this->_cache->flush(); - } + return $this->useMemcached ? $this->_cache->add($key, $value, $expire) : $this->_cache->add($key, $value, 0, $expire); + } + + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return $this->_cache->delete($key, 0); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + return $this->_cache->flush(); + } } diff --git a/framework/caching/MemCacheServer.php b/framework/caching/MemCacheServer.php index b1b77278c40..05da7a33ea6 100644 --- a/framework/caching/MemCacheServer.php +++ b/framework/caching/MemCacheServer.php @@ -18,41 +18,41 @@ */ class MemCacheServer extends \yii\base\Object { - /** - * @var string memcache server hostname or IP address - */ - public $host; - /** - * @var integer memcache server port - */ - public $port = 11211; - /** - * @var integer probability of using this server among all servers. - */ - public $weight = 1; - /** - * @var boolean whether to use a persistent connection. This is used by memcache only. - */ - public $persistent = true; - /** - * @var integer timeout in milliseconds which will be used for connecting to the server. - * This is used by memcache only. For old versions of memcache that only support specifying - * timeout in seconds this will be rounded up to full seconds. - */ - public $timeout = 1000; - /** - * @var integer how often a failed server will be retried (in seconds). This is used by memcache only. - */ - public $retryInterval = 15; - /** - * @var boolean if the server should be flagged as online upon a failure. This is used by memcache only. - */ - public $status = true; - /** - * @var \Closure this callback function will run upon encountering an error. - * The callback is run before fail over is attempted. The function takes two parameters, - * the [[host]] and the [[port]] of the failed server. - * This is used by memcache only. - */ - public $failureCallback; + /** + * @var string memcache server hostname or IP address + */ + public $host; + /** + * @var integer memcache server port + */ + public $port = 11211; + /** + * @var integer probability of using this server among all servers. + */ + public $weight = 1; + /** + * @var boolean whether to use a persistent connection. This is used by memcache only. + */ + public $persistent = true; + /** + * @var integer timeout in milliseconds which will be used for connecting to the server. + * This is used by memcache only. For old versions of memcache that only support specifying + * timeout in seconds this will be rounded up to full seconds. + */ + public $timeout = 1000; + /** + * @var integer how often a failed server will be retried (in seconds). This is used by memcache only. + */ + public $retryInterval = 15; + /** + * @var boolean if the server should be flagged as online upon a failure. This is used by memcache only. + */ + public $status = true; + /** + * @var \Closure this callback function will run upon encountering an error. + * The callback is run before fail over is attempted. The function takes two parameters, + * the [[host]] and the [[port]] of the failed server. + * This is used by memcache only. + */ + public $failureCallback; } diff --git a/framework/caching/WinCache.php b/framework/caching/WinCache.php index 6b80f3cc047..4c611c2f8ff 100644 --- a/framework/caching/WinCache.php +++ b/framework/caching/WinCache.php @@ -20,113 +20,114 @@ */ class WinCache extends Cache { - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - $key = $this->buildKey($key); - return wincache_ucache_exists($key); - } + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - return wincache_ucache_get($key); - } + return wincache_ucache_exists($key); + } - /** - * Retrieves multiple values from cache with the specified keys. - * @param array $keys a list of keys identifying the cached values - * @return array a list of cached values indexed by the keys - */ - protected function getValues($keys) - { - return wincache_ucache_get($keys); - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return wincache_ucache_get($key); + } - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - return wincache_ucache_set($key, $value, $expire); - } + /** + * Retrieves multiple values from cache with the specified keys. + * @param array $keys a list of keys identifying the cached values + * @return array a list of cached values indexed by the keys + */ + protected function getValues($keys) + { + return wincache_ucache_get($keys); + } - /** - * Stores multiple key-value pairs in cache. - * @param array $data array where key corresponds to cache key while value is the value stored - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys - */ - protected function setValues($data, $expire) - { - return wincache_ucache_set($data, null, $expire); - } + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + return wincache_ucache_set($key, $value, $expire); + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - return wincache_ucache_add($key, $value, $expire); - } + /** + * Stores multiple key-value pairs in cache. + * @param array $data array where key corresponds to cache key while value is the value stored + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys + */ + protected function setValues($data, $expire) + { + return wincache_ucache_set($data, null, $expire); + } - /** - * Adds multiple key-value pairs to cache. - * The default implementation calls [[addValue()]] multiple times add values one by one. If the underlying cache - * storage supports multiadd, this method should be overridden to exploit that feature. - * @param array $data array where key corresponds to cache key while value is the value stored - * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. - * @return array array of failed keys - */ - protected function addValues($data, $expire) - { - return wincache_ucache_add($data, null, $expire); - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + return wincache_ucache_add($key, $value, $expire); + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - return wincache_ucache_delete($key); - } + /** + * Adds multiple key-value pairs to cache. + * The default implementation calls [[addValue()]] multiple times add values one by one. If the underlying cache + * storage supports multiadd, this method should be overridden to exploit that feature. + * @param array $data array where key corresponds to cache key while value is the value stored + * @param integer $expire the number of seconds in which the cached values will expire. 0 means never expire. + * @return array array of failed keys + */ + protected function addValues($data, $expire) + { + return wincache_ucache_add($data, null, $expire); + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - return wincache_ucache_clear(); - } + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return wincache_ucache_delete($key); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + return wincache_ucache_clear(); + } } diff --git a/framework/caching/XCache.php b/framework/caching/XCache.php index 4221a398631..370d4cea0c7 100644 --- a/framework/caching/XCache.php +++ b/framework/caching/XCache.php @@ -21,84 +21,86 @@ */ class XCache extends Cache { - /** - * Checks whether a specified key exists in the cache. - * This can be faster than getting the value from the cache if the data is big. - * Note that this method does not check whether the dependency associated - * with the cached data, if there is any, has changed. So a call to [[get]] - * may return false while exists returns true. - * @param mixed $key a key identifying the cached value. This can be a simple string or - * a complex data structure consisting of factors representing the key. - * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. - */ - public function exists($key) - { - $key = $this->buildKey($key); - return xcache_isset($key); - } + /** + * Checks whether a specified key exists in the cache. + * This can be faster than getting the value from the cache if the data is big. + * Note that this method does not check whether the dependency associated + * with the cached data, if there is any, has changed. So a call to [[get]] + * may return false while exists returns true. + * @param mixed $key a key identifying the cached value. This can be a simple string or + * a complex data structure consisting of factors representing the key. + * @return boolean true if a value exists in cache, false if the value is not in the cache or expired. + */ + public function exists($key) + { + $key = $this->buildKey($key); - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - return xcache_isset($key) ? xcache_get($key) : false; - } + return xcache_isset($key); + } - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - return xcache_set($key, $value, $expire); - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + return xcache_isset($key) ? xcache_get($key) : false; + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - return !xcache_isset($key) ? $this->setValue($key, $value, $expire) : false; - } + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + return xcache_set($key, $value, $expire); + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - return xcache_unset($key); - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + return !xcache_isset($key) ? $this->setValue($key, $value, $expire) : false; + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - for ($i = 0, $max = xcache_count(XC_TYPE_VAR); $i < $max; $i++) { - if (xcache_clear_cache(XC_TYPE_VAR, $i) === false) { - return false; - } - } - return true; - } + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return xcache_unset($key); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + for ($i = 0, $max = xcache_count(XC_TYPE_VAR); $i < $max; $i++) { + if (xcache_clear_cache(XC_TYPE_VAR, $i) === false) { + return false; + } + } + + return true; + } } diff --git a/framework/caching/ZendDataCache.php b/framework/caching/ZendDataCache.php index 9ff2fd057af..f06459eb0a1 100644 --- a/framework/caching/ZendDataCache.php +++ b/framework/caching/ZendDataCache.php @@ -20,64 +20,65 @@ */ class ZendDataCache extends Cache { - /** - * Retrieves a value from cache with a specified key. - * This is the implementation of the method declared in the parent class. - * @param string $key a unique key identifying the cached value - * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. - */ - protected function getValue($key) - { - $result = zend_shm_cache_fetch($key); - return $result === null ? false : $result; - } + /** + * Retrieves a value from cache with a specified key. + * This is the implementation of the method declared in the parent class. + * @param string $key a unique key identifying the cached value + * @return string|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + protected function getValue($key) + { + $result = zend_shm_cache_fetch($key); - /** - * Stores a value identified by a key in cache. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function setValue($key, $value, $expire) - { - return zend_shm_cache_store($key, $value, $expire); - } + return $result === null ? false : $result; + } - /** - * Stores a value identified by a key into cache if the cache does not contain this key. - * This is the implementation of the method declared in the parent class. - * - * @param string $key the key identifying the value to be cached - * @param string $value the value to be cached - * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. - * @return boolean true if the value is successfully stored into cache, false otherwise - */ - protected function addValue($key, $value, $expire) - { - return zend_shm_cache_fetch($key) === null ? $this->setValue($key, $value, $expire) : false; - } + /** + * Stores a value identified by a key in cache. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function setValue($key, $value, $expire) + { + return zend_shm_cache_store($key, $value, $expire); + } - /** - * Deletes a value with the specified key from cache - * This is the implementation of the method declared in the parent class. - * @param string $key the key of the value to be deleted - * @return boolean if no error happens during deletion - */ - protected function deleteValue($key) - { - return zend_shm_cache_delete($key); - } + /** + * Stores a value identified by a key into cache if the cache does not contain this key. + * This is the implementation of the method declared in the parent class. + * + * @param string $key the key identifying the value to be cached + * @param string $value the value to be cached + * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + protected function addValue($key, $value, $expire) + { + return zend_shm_cache_fetch($key) === null ? $this->setValue($key, $value, $expire) : false; + } - /** - * Deletes all values from cache. - * This is the implementation of the method declared in the parent class. - * @return boolean whether the flush operation was successful. - */ - protected function flushValues() - { - return zend_shm_cache_clear(); - } + /** + * Deletes a value with the specified key from cache + * This is the implementation of the method declared in the parent class. + * @param string $key the key of the value to be deleted + * @return boolean if no error happens during deletion + */ + protected function deleteValue($key) + { + return zend_shm_cache_delete($key); + } + + /** + * Deletes all values from cache. + * This is the implementation of the method declared in the parent class. + * @return boolean whether the flush operation was successful. + */ + protected function flushValues() + { + return zend_shm_cache_clear(); + } } diff --git a/framework/captcha/Captcha.php b/framework/captcha/Captcha.php index 03e2145a84c..61bd75d1fad 100644 --- a/framework/captcha/Captcha.php +++ b/framework/captcha/Captcha.php @@ -34,109 +34,109 @@ */ class Captcha extends InputWidget { - /** - * @var string the route of the action that generates the CAPTCHA images. - * The action represented by this route must be an action of [[CaptchaAction]]. - */ - public $captchaAction = 'site/captcha'; - /** - * @var array HTML attributes to be applied to the CAPTCHA image tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $imageOptions = []; - /** - * @var string the template for arranging the CAPTCHA image tag and the text input tag. - * In this template, the token `{image}` will be replaced with the actual image tag, - * while `{input}` will be replaced with the text input tag. - */ - public $template = '{image} {input}'; - /** - * @var array the HTML attributes for the input tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'form-control']; + /** + * @var string the route of the action that generates the CAPTCHA images. + * The action represented by this route must be an action of [[CaptchaAction]]. + */ + public $captchaAction = 'site/captcha'; + /** + * @var array HTML attributes to be applied to the CAPTCHA image tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $imageOptions = []; + /** + * @var string the template for arranging the CAPTCHA image tag and the text input tag. + * In this template, the token `{image}` will be replaced with the actual image tag, + * while `{input}` will be replaced with the text input tag. + */ + public $template = '{image} {input}'; + /** + * @var array the HTML attributes for the input tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'form-control']; + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); - /** - * Initializes the widget. - */ - public function init() - { - parent::init(); + $this->checkRequirements(); - $this->checkRequirements(); + if (!isset($this->imageOptions['id'])) { + $this->imageOptions['id'] = $this->options['id'] . '-image'; + } + } - if (!isset($this->imageOptions['id'])) { - $this->imageOptions['id'] = $this->options['id'] . '-image'; - } - } + /** + * Renders the widget. + */ + public function run() + { + $this->registerClientScript(); + if ($this->hasModel()) { + $input = Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + $input = Html::textInput($this->name, $this->value, $this->options); + } + $url = Yii::$app->getUrlManager()->createUrl([$this->captchaAction, 'v' => uniqid()]); + $image = Html::img($url, $this->imageOptions); + echo strtr($this->template, [ + '{input}' => $input, + '{image}' => $image, + ]); + } - /** - * Renders the widget. - */ - public function run() - { - $this->registerClientScript(); - if ($this->hasModel()) { - $input = Html::activeTextInput($this->model, $this->attribute, $this->options); - } else { - $input = Html::textInput($this->name, $this->value, $this->options); - } - $url = Yii::$app->getUrlManager()->createUrl([$this->captchaAction, 'v' => uniqid()]); - $image = Html::img($url, $this->imageOptions); - echo strtr($this->template, [ - '{input}' => $input, - '{image}' => $image, - ]); - } + /** + * Registers the needed JavaScript. + */ + public function registerClientScript() + { + $options = $this->getClientOptions(); + $options = empty($options) ? '' : Json::encode($options); + $id = $this->imageOptions['id']; + $view = $this->getView(); + CaptchaAsset::register($view); + $view->registerJs("jQuery('#$id').yiiCaptcha($options);"); + } - /** - * Registers the needed JavaScript. - */ - public function registerClientScript() - { - $options = $this->getClientOptions(); - $options = empty($options) ? '' : Json::encode($options); - $id = $this->imageOptions['id']; - $view = $this->getView(); - CaptchaAsset::register($view); - $view->registerJs("jQuery('#$id').yiiCaptcha($options);"); - } + /** + * Returns the options for the captcha JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $options = [ + 'refreshUrl' => Url::to(['/' . $this->captchaAction, CaptchaAction::REFRESH_GET_VAR => 1]), + 'hashKey' => "yiiCaptcha/{$this->captchaAction}", + ]; - /** - * Returns the options for the captcha JS widget. - * @return array the options - */ - protected function getClientOptions() - { - $options = [ - 'refreshUrl' => Url::to(['/' . $this->captchaAction, CaptchaAction::REFRESH_GET_VAR => 1]), - 'hashKey' => "yiiCaptcha/{$this->captchaAction}", - ]; - return $options; - } + return $options; + } - /** - * Checks if there is graphic extension available to generate CAPTCHA images. - * This method will check the existence of ImageMagick and GD extensions. - * @return string the name of the graphic extension, either "imagick" or "gd". - * @throws InvalidConfigException if neither ImageMagick nor GD is installed. - */ - public static function checkRequirements() - { - if (extension_loaded('imagick')) { - $imagick = new \Imagick(); - $imagickFormats = $imagick->queryFormats('PNG'); - if (in_array('PNG', $imagickFormats)) { - return 'imagick'; - } - } - if (extension_loaded('gd')) { - $gdInfo = gd_info(); - if (!empty($gdInfo['FreeType Support'])) { - return 'gd'; - } - } - throw new InvalidConfigException('GD with FreeType or ImageMagick PHP extensions are required.'); - } + /** + * Checks if there is graphic extension available to generate CAPTCHA images. + * This method will check the existence of ImageMagick and GD extensions. + * @return string the name of the graphic extension, either "imagick" or "gd". + * @throws InvalidConfigException if neither ImageMagick nor GD is installed. + */ + public static function checkRequirements() + { + if (extension_loaded('imagick')) { + $imagick = new \Imagick(); + $imagickFormats = $imagick->queryFormats('PNG'); + if (in_array('PNG', $imagickFormats)) { + return 'imagick'; + } + } + if (extension_loaded('gd')) { + $gdInfo = gd_info(); + if (!empty($gdInfo['FreeType Support'])) { + return 'gd'; + } + } + throw new InvalidConfigException('GD with FreeType or ImageMagick PHP extensions are required.'); + } } diff --git a/framework/captcha/CaptchaAction.php b/framework/captcha/CaptchaAction.php index ad7958b83a0..ba2255371eb 100644 --- a/framework/captcha/CaptchaAction.php +++ b/framework/captcha/CaptchaAction.php @@ -37,304 +37,310 @@ */ class CaptchaAction extends Action { - /** - * The name of the GET parameter indicating whether the CAPTCHA image should be regenerated. - */ - const REFRESH_GET_VAR = 'refresh'; - /** - * @var integer how many times should the same CAPTCHA be displayed. Defaults to 3. - * A value less than or equal to 0 means the test is unlimited (available since version 1.1.2). - */ - public $testLimit = 3; - /** - * @var integer the width of the generated CAPTCHA image. Defaults to 120. - */ - public $width = 120; - /** - * @var integer the height of the generated CAPTCHA image. Defaults to 50. - */ - public $height = 50; - /** - * @var integer padding around the text. Defaults to 2. - */ - public $padding = 2; - /** - * @var integer the background color. For example, 0x55FF00. - * Defaults to 0xFFFFFF, meaning white color. - */ - public $backColor = 0xFFFFFF; - /** - * @var integer the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color). - */ - public $foreColor = 0x2040A0; - /** - * @var boolean whether to use transparent background. Defaults to false. - */ - public $transparent = false; - /** - * @var integer the minimum length for randomly generated word. Defaults to 6. - */ - public $minLength = 6; - /** - * @var integer the maximum length for randomly generated word. Defaults to 7. - */ - public $maxLength = 7; - /** - * @var integer the offset between characters. Defaults to -2. You can adjust this property - * in order to decrease or increase the readability of the captcha. - **/ - public $offset = -2; - /** - * @var string the TrueType font file. This can be either a file path or path alias. - */ - public $fontFile = '@yii/captcha/SpicyRice.ttf'; - /** - * @var string the fixed verification code. When this property is set, - * [[getVerifyCode()]] will always return the value of this property. - * This is mainly used in automated tests where we want to be able to reproduce - * the same verification code each time we run the tests. - * If not set, it means the verification code will be randomly generated. - */ - public $fixedVerifyCode; - - - /** - * Initializes the action. - * @throws InvalidConfigException if the font file does not exist. - */ - public function init() - { - $this->fontFile = Yii::getAlias($this->fontFile); - if (!is_file($this->fontFile)) { - throw new InvalidConfigException("The font file does not exist: {$this->fontFile}"); - } - } - - /** - * Runs the action. - */ - public function run() - { - if (Yii::$app->request->getQueryParam(self::REFRESH_GET_VAR) !== null) { - // AJAX request for regenerating code - $code = $this->getVerifyCode(true); - return json_encode([ - 'hash1' => $this->generateValidationHash($code), - 'hash2' => $this->generateValidationHash(strtolower($code)), - // we add a random 'v' parameter so that FireFox can refresh the image - // when src attribute of image tag is changed - 'url' => Url::to([$this->id, 'v' => uniqid()]), - ]); - } else { - $this->setHttpHeaders(); - return $this->renderImage($this->getVerifyCode()); - } - } - - /** - * Generates a hash code that can be used for client side validation. - * @param string $code the CAPTCHA code - * @return string a hash code generated from the CAPTCHA code - */ - public function generateValidationHash($code) - { - for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) { - $h += ord($code[$i]); - } - return $h; - } - - /** - * Gets the verification code. - * @param boolean $regenerate whether the verification code should be regenerated. - * @return string the verification code. - */ - public function getVerifyCode($regenerate = false) - { - if ($this->fixedVerifyCode !== null) { - return $this->fixedVerifyCode; - } - - $session = Yii::$app->getSession(); - $session->open(); - $name = $this->getSessionKey(); - if ($session[$name] === null || $regenerate) { - $session[$name] = $this->generateVerifyCode(); - $session[$name . 'count'] = 1; - } - return $session[$name]; - } - - /** - * Validates the input to see if it matches the generated code. - * @param string $input user input - * @param boolean $caseSensitive whether the comparison should be case-sensitive - * @return boolean whether the input is valid - */ - public function validate($input, $caseSensitive) - { - $code = $this->getVerifyCode(); - $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0; - $session = Yii::$app->getSession(); - $session->open(); - $name = $this->getSessionKey() . 'count'; - $session[$name] = $session[$name] + 1; - if ($valid || $session[$name] > $this->testLimit && $this->testLimit > 0) { - $this->getVerifyCode(true); - } - return $valid; - } - - /** - * Generates a new verification code. - * @return string the generated verification code - */ - protected function generateVerifyCode() - { - if ($this->minLength > $this->maxLength) { - $this->maxLength = $this->minLength; - } - if ($this->minLength < 3) { - $this->minLength = 3; - } - if ($this->maxLength > 20) { - $this->maxLength = 20; - } - $length = mt_rand($this->minLength, $this->maxLength); - - $letters = 'bcdfghjklmnpqrstvwxyz'; - $vowels = 'aeiou'; - $code = ''; - for ($i = 0; $i < $length; ++$i) { - if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) { - $code .= $vowels[mt_rand(0, 4)]; - } else { - $code .= $letters[mt_rand(0, 20)]; - } - } - - return $code; - } - - /** - * Returns the session variable name used to store verification code. - * @return string the session variable name - */ - protected function getSessionKey() - { - return '__captcha/' . $this->getUniqueId(); - } - - /** - * Renders the CAPTCHA image. - * @param string $code the verification code - * @return string image contents - */ - protected function renderImage($code) - { - if (Captcha::checkRequirements() === 'gd') { - return $this->renderImageByGD($code); - } else { - return $this->renderImageByImagick($code); - } - } - - /** - * Renders the CAPTCHA image based on the code using GD library. - * @param string $code the verification code - * @return string image contents - */ - protected function renderImageByGD($code) - { - $image = imagecreatetruecolor($this->width, $this->height); - - $backColor = imagecolorallocate($image, - (int)($this->backColor % 0x1000000 / 0x10000), - (int)($this->backColor % 0x10000 / 0x100), - $this->backColor % 0x100); - imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backColor); - imagecolordeallocate($image, $backColor); - - if ($this->transparent) { - imagecolortransparent($image, $backColor); - } - - $foreColor = imagecolorallocate($image, - (int)($this->foreColor % 0x1000000 / 0x10000), - (int)($this->foreColor % 0x10000 / 0x100), - $this->foreColor % 0x100); - - $length = strlen($code); - $box = imagettfbbox(30, 0, $this->fontFile, $code); - $w = $box[4] - $box[0] + $this->offset * ($length - 1); - $h = $box[1] - $box[5]; - $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); - $x = 10; - $y = round($this->height * 27 / 40); - for ($i = 0; $i < $length; ++$i) { - $fontSize = (int)(rand(26, 32) * $scale * 0.8); - $angle = rand(-10, 10); - $letter = $code[$i]; - $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter); - $x = $box[2] + $this->offset; - } - - imagecolordeallocate($image, $foreColor); - - ob_start(); - imagepng($image); - imagedestroy($image); - return ob_get_clean(); - } - - /** - * Renders the CAPTCHA image based on the code using ImageMagick library. - * @param string $code the verification code - * @return \Imagick image instance. Can be used as string. In this case it will contain image contents. - */ - protected function renderImageByImagick($code) - { - $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . dechex($this->backColor)); - $foreColor = new \ImagickPixel('#' . dechex($this->foreColor)); - - $image = new \Imagick(); - $image->newImage($this->width, $this->height, $backColor); - - $draw = new \ImagickDraw(); - $draw->setFont($this->fontFile); - $draw->setFontSize(30); - $fontMetrics = $image->queryFontMetrics($draw, $code); - - $length = strlen($code); - $w = (int)($fontMetrics['textWidth']) - 8 + $this->offset * ($length - 1); - $h = (int)($fontMetrics['textHeight']) - 8; - $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); - $x = 10; - $y = round($this->height * 27 / 40); - for ($i = 0; $i < $length; ++$i) { - $draw = new \ImagickDraw(); - $draw->setFont($this->fontFile); - $draw->setFontSize((int)(rand(26, 32) * $scale * 0.8)); - $draw->setFillColor($foreColor); - $image->annotateImage($draw, $x, $y, rand(-10, 10), $code[$i]); - $fontMetrics = $image->queryFontMetrics($draw, $code[$i]); - $x += (int)($fontMetrics['textWidth']) + $this->offset; - } - - $image->setImageFormat('png'); - return $image; - } - - /** - * Sets the HTTP headers needed by image response. - */ - protected function setHttpHeaders() - { - Yii::$app->getResponse()->getHeaders() - ->set('Pragma', 'public') - ->set('Expires', '0') - ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->set('Content-Transfer-Encoding', 'binary') - ->set('Content-type', 'image/png'); - } + /** + * The name of the GET parameter indicating whether the CAPTCHA image should be regenerated. + */ + const REFRESH_GET_VAR = 'refresh'; + /** + * @var integer how many times should the same CAPTCHA be displayed. Defaults to 3. + * A value less than or equal to 0 means the test is unlimited (available since version 1.1.2). + */ + public $testLimit = 3; + /** + * @var integer the width of the generated CAPTCHA image. Defaults to 120. + */ + public $width = 120; + /** + * @var integer the height of the generated CAPTCHA image. Defaults to 50. + */ + public $height = 50; + /** + * @var integer padding around the text. Defaults to 2. + */ + public $padding = 2; + /** + * @var integer the background color. For example, 0x55FF00. + * Defaults to 0xFFFFFF, meaning white color. + */ + public $backColor = 0xFFFFFF; + /** + * @var integer the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color). + */ + public $foreColor = 0x2040A0; + /** + * @var boolean whether to use transparent background. Defaults to false. + */ + public $transparent = false; + /** + * @var integer the minimum length for randomly generated word. Defaults to 6. + */ + public $minLength = 6; + /** + * @var integer the maximum length for randomly generated word. Defaults to 7. + */ + public $maxLength = 7; + /** + * @var integer the offset between characters. Defaults to -2. You can adjust this property + * in order to decrease or increase the readability of the captcha. + **/ + public $offset = -2; + /** + * @var string the TrueType font file. This can be either a file path or path alias. + */ + public $fontFile = '@yii/captcha/SpicyRice.ttf'; + /** + * @var string the fixed verification code. When this property is set, + * [[getVerifyCode()]] will always return the value of this property. + * This is mainly used in automated tests where we want to be able to reproduce + * the same verification code each time we run the tests. + * If not set, it means the verification code will be randomly generated. + */ + public $fixedVerifyCode; + + /** + * Initializes the action. + * @throws InvalidConfigException if the font file does not exist. + */ + public function init() + { + $this->fontFile = Yii::getAlias($this->fontFile); + if (!is_file($this->fontFile)) { + throw new InvalidConfigException("The font file does not exist: {$this->fontFile}"); + } + } + + /** + * Runs the action. + */ + public function run() + { + if (Yii::$app->request->getQueryParam(self::REFRESH_GET_VAR) !== null) { + // AJAX request for regenerating code + $code = $this->getVerifyCode(true); + + return json_encode([ + 'hash1' => $this->generateValidationHash($code), + 'hash2' => $this->generateValidationHash(strtolower($code)), + // we add a random 'v' parameter so that FireFox can refresh the image + // when src attribute of image tag is changed + 'url' => Url::to([$this->id, 'v' => uniqid()]), + ]); + } else { + $this->setHttpHeaders(); + + return $this->renderImage($this->getVerifyCode()); + } + } + + /** + * Generates a hash code that can be used for client side validation. + * @param string $code the CAPTCHA code + * @return string a hash code generated from the CAPTCHA code + */ + public function generateValidationHash($code) + { + for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) { + $h += ord($code[$i]); + } + + return $h; + } + + /** + * Gets the verification code. + * @param boolean $regenerate whether the verification code should be regenerated. + * @return string the verification code. + */ + public function getVerifyCode($regenerate = false) + { + if ($this->fixedVerifyCode !== null) { + return $this->fixedVerifyCode; + } + + $session = Yii::$app->getSession(); + $session->open(); + $name = $this->getSessionKey(); + if ($session[$name] === null || $regenerate) { + $session[$name] = $this->generateVerifyCode(); + $session[$name . 'count'] = 1; + } + + return $session[$name]; + } + + /** + * Validates the input to see if it matches the generated code. + * @param string $input user input + * @param boolean $caseSensitive whether the comparison should be case-sensitive + * @return boolean whether the input is valid + */ + public function validate($input, $caseSensitive) + { + $code = $this->getVerifyCode(); + $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0; + $session = Yii::$app->getSession(); + $session->open(); + $name = $this->getSessionKey() . 'count'; + $session[$name] = $session[$name] + 1; + if ($valid || $session[$name] > $this->testLimit && $this->testLimit > 0) { + $this->getVerifyCode(true); + } + + return $valid; + } + + /** + * Generates a new verification code. + * @return string the generated verification code + */ + protected function generateVerifyCode() + { + if ($this->minLength > $this->maxLength) { + $this->maxLength = $this->minLength; + } + if ($this->minLength < 3) { + $this->minLength = 3; + } + if ($this->maxLength > 20) { + $this->maxLength = 20; + } + $length = mt_rand($this->minLength, $this->maxLength); + + $letters = 'bcdfghjklmnpqrstvwxyz'; + $vowels = 'aeiou'; + $code = ''; + for ($i = 0; $i < $length; ++$i) { + if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) { + $code .= $vowels[mt_rand(0, 4)]; + } else { + $code .= $letters[mt_rand(0, 20)]; + } + } + + return $code; + } + + /** + * Returns the session variable name used to store verification code. + * @return string the session variable name + */ + protected function getSessionKey() + { + return '__captcha/' . $this->getUniqueId(); + } + + /** + * Renders the CAPTCHA image. + * @param string $code the verification code + * @return string image contents + */ + protected function renderImage($code) + { + if (Captcha::checkRequirements() === 'gd') { + return $this->renderImageByGD($code); + } else { + return $this->renderImageByImagick($code); + } + } + + /** + * Renders the CAPTCHA image based on the code using GD library. + * @param string $code the verification code + * @return string image contents + */ + protected function renderImageByGD($code) + { + $image = imagecreatetruecolor($this->width, $this->height); + + $backColor = imagecolorallocate($image, + (int) ($this->backColor % 0x1000000 / 0x10000), + (int) ($this->backColor % 0x10000 / 0x100), + $this->backColor % 0x100); + imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backColor); + imagecolordeallocate($image, $backColor); + + if ($this->transparent) { + imagecolortransparent($image, $backColor); + } + + $foreColor = imagecolorallocate($image, + (int) ($this->foreColor % 0x1000000 / 0x10000), + (int) ($this->foreColor % 0x10000 / 0x100), + $this->foreColor % 0x100); + + $length = strlen($code); + $box = imagettfbbox(30, 0, $this->fontFile, $code); + $w = $box[4] - $box[0] + $this->offset * ($length - 1); + $h = $box[1] - $box[5]; + $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); + $x = 10; + $y = round($this->height * 27 / 40); + for ($i = 0; $i < $length; ++$i) { + $fontSize = (int) (rand(26, 32) * $scale * 0.8); + $angle = rand(-10, 10); + $letter = $code[$i]; + $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter); + $x = $box[2] + $this->offset; + } + + imagecolordeallocate($image, $foreColor); + + ob_start(); + imagepng($image); + imagedestroy($image); + + return ob_get_clean(); + } + + /** + * Renders the CAPTCHA image based on the code using ImageMagick library. + * @param string $code the verification code + * @return \Imagick image instance. Can be used as string. In this case it will contain image contents. + */ + protected function renderImageByImagick($code) + { + $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . dechex($this->backColor)); + $foreColor = new \ImagickPixel('#' . dechex($this->foreColor)); + + $image = new \Imagick(); + $image->newImage($this->width, $this->height, $backColor); + + $draw = new \ImagickDraw(); + $draw->setFont($this->fontFile); + $draw->setFontSize(30); + $fontMetrics = $image->queryFontMetrics($draw, $code); + + $length = strlen($code); + $w = (int) ($fontMetrics['textWidth']) - 8 + $this->offset * ($length - 1); + $h = (int) ($fontMetrics['textHeight']) - 8; + $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); + $x = 10; + $y = round($this->height * 27 / 40); + for ($i = 0; $i < $length; ++$i) { + $draw = new \ImagickDraw(); + $draw->setFont($this->fontFile); + $draw->setFontSize((int) (rand(26, 32) * $scale * 0.8)); + $draw->setFillColor($foreColor); + $image->annotateImage($draw, $x, $y, rand(-10, 10), $code[$i]); + $fontMetrics = $image->queryFontMetrics($draw, $code[$i]); + $x += (int) ($fontMetrics['textWidth']) + $this->offset; + } + + $image->setImageFormat('png'); + + return $image; + } + + /** + * Sets the HTTP headers needed by image response. + */ + protected function setHttpHeaders() + { + Yii::$app->getResponse()->getHeaders() + ->set('Pragma', 'public') + ->set('Expires', '0') + ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->set('Content-Transfer-Encoding', 'binary') + ->set('Content-type', 'image/png'); + } } diff --git a/framework/captcha/CaptchaAsset.php b/framework/captcha/CaptchaAsset.php index 4fc722f1da6..8af07e17a9f 100644 --- a/framework/captcha/CaptchaAsset.php +++ b/framework/captcha/CaptchaAsset.php @@ -17,11 +17,11 @@ */ class CaptchaAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'yii.captcha.js', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'yii.captcha.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/framework/captcha/CaptchaValidator.php b/framework/captcha/CaptchaValidator.php index acef79d8bb8..0be46d3be1f 100644 --- a/framework/captcha/CaptchaValidator.php +++ b/framework/captcha/CaptchaValidator.php @@ -26,81 +26,82 @@ */ class CaptchaValidator extends Validator { - /** - * @var boolean whether to skip this validator if the input is empty. - */ - public $skipOnEmpty = false; - /** - * @var boolean whether the comparison is case sensitive. Defaults to false. - */ - public $caseSensitive = false; - /** - * @var string the route of the controller action that renders the CAPTCHA image. - */ - public $captchaAction = 'site/captcha'; + /** + * @var boolean whether to skip this validator if the input is empty. + */ + public $skipOnEmpty = false; + /** + * @var boolean whether the comparison is case sensitive. Defaults to false. + */ + public $caseSensitive = false; + /** + * @var string the route of the controller action that renders the CAPTCHA image. + */ + public $captchaAction = 'site/captcha'; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii', 'The verification code is incorrect.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = Yii::t('yii', 'The verification code is incorrect.'); - } - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + $captcha = $this->createCaptchaAction(); + $valid = !is_array($value) && $captcha->validate($value, $this->caseSensitive); - /** - * @inheritdoc - */ - protected function validateValue($value) - { - $captcha = $this->createCaptchaAction(); - $valid = !is_array($value) && $captcha->validate($value, $this->caseSensitive); - return $valid ? null : [$this->message, []]; - } + return $valid ? null : [$this->message, []]; + } - /** - * Creates the CAPTCHA action object from the route specified by [[captchaAction]]. - * @return \yii\captcha\CaptchaAction the action object - * @throws InvalidConfigException - */ - public function createCaptchaAction() - { - $ca = Yii::$app->createController($this->captchaAction); - if ($ca !== false) { - /** @var \yii\base\Controller $controller */ - list($controller, $actionID) = $ca; - $action = $controller->createAction($actionID); - if ($action !== null) { - return $action; - } - } - throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction); - } + /** + * Creates the CAPTCHA action object from the route specified by [[captchaAction]]. + * @return \yii\captcha\CaptchaAction the action object + * @throws InvalidConfigException + */ + public function createCaptchaAction() + { + $ca = Yii::$app->createController($this->captchaAction); + if ($ca !== false) { + /** @var \yii\base\Controller $controller */ + list($controller, $actionID) = $ca; + $action = $controller->createAction($actionID); + if ($action !== null) { + return $action; + } + } + throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction); + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $captcha = $this->createCaptchaAction(); - $code = $captcha->getVerifyCode(false); - $hash = $captcha->generateValidationHash($this->caseSensitive ? $code : strtolower($code)); - $options = [ - 'hash' => $hash, - 'hashKey' => 'yiiCaptcha/' . $this->captchaAction, - 'caseSensitive' => $this->caseSensitive, - 'message' => strtr($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - ]), - ]; - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $captcha = $this->createCaptchaAction(); + $code = $captcha->getVerifyCode(false); + $hash = $captcha->generateValidationHash($this->caseSensitive ? $code : strtolower($code)); + $options = [ + 'hash' => $hash, + 'hashKey' => 'yiiCaptcha/' . $this->captchaAction, + 'caseSensitive' => $this->caseSensitive, + 'message' => strtr($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + ]), + ]; + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } - ValidationAsset::register($view); - return 'yii.validation.captcha(value, messages, ' . json_encode($options) . ');'; - } + ValidationAsset::register($view); + + return 'yii.validation.captcha(value, messages, ' . json_encode($options) . ');'; + } } diff --git a/framework/classes.php b/framework/classes.php index 552b943f68b..3589a8ca30a 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -11,265 +11,265 @@ */ return [ - 'yii\base\Action' => YII_PATH . '/base/Action.php', - 'yii\base\ActionEvent' => YII_PATH . '/base/ActionEvent.php', - 'yii\base\ActionFilter' => YII_PATH . '/base/ActionFilter.php', - 'yii\base\Application' => YII_PATH . '/base/Application.php', - 'yii\base\ArrayAccessTrait' => YII_PATH . '/base/ArrayAccessTrait.php', - 'yii\base\Arrayable' => YII_PATH . '/base/Arrayable.php', - 'yii\base\Behavior' => YII_PATH . '/base/Behavior.php', - 'yii\base\Component' => YII_PATH . '/base/Component.php', - 'yii\base\Controller' => YII_PATH . '/base/Controller.php', - 'yii\base\DynamicModel' => YII_PATH . '/base/DynamicModel.php', - 'yii\base\ErrorException' => YII_PATH . '/base/ErrorException.php', - 'yii\base\ErrorHandler' => YII_PATH . '/base/ErrorHandler.php', - 'yii\base\Event' => YII_PATH . '/base/Event.php', - 'yii\base\Exception' => YII_PATH . '/base/Exception.php', - 'yii\base\Extension' => YII_PATH . '/base/Extension.php', - 'yii\base\Formatter' => YII_PATH . '/base/Formatter.php', - 'yii\base\InlineAction' => YII_PATH . '/base/InlineAction.php', - 'yii\base\InvalidCallException' => YII_PATH . '/base/InvalidCallException.php', - 'yii\base\InvalidConfigException' => YII_PATH . '/base/InvalidConfigException.php', - 'yii\base\InvalidParamException' => YII_PATH . '/base/InvalidParamException.php', - 'yii\base\InvalidRouteException' => YII_PATH . '/base/InvalidRouteException.php', - 'yii\base\MailEvent' => YII_PATH . '/base/MailEvent.php', - 'yii\base\Model' => YII_PATH . '/base/Model.php', - 'yii\base\ModelEvent' => YII_PATH . '/base/ModelEvent.php', - 'yii\base\Module' => YII_PATH . '/base/Module.php', - 'yii\base\NotSupportedException' => YII_PATH . '/base/NotSupportedException.php', - 'yii\base\Object' => YII_PATH . '/base/Object.php', - 'yii\base\Request' => YII_PATH . '/base/Request.php', - 'yii\base\Response' => YII_PATH . '/base/Response.php', - 'yii\base\Theme' => YII_PATH . '/base/Theme.php', - 'yii\base\UnknownClassException' => YII_PATH . '/base/UnknownClassException.php', - 'yii\base\UnknownMethodException' => YII_PATH . '/base/UnknownMethodException.php', - 'yii\base\UnknownPropertyException' => YII_PATH . '/base/UnknownPropertyException.php', - 'yii\base\UserException' => YII_PATH . '/base/UserException.php', - 'yii\base\View' => YII_PATH . '/base/View.php', - 'yii\base\ViewContextInterface' => YII_PATH . '/base/ViewContextInterface.php', - 'yii\base\ViewEvent' => YII_PATH . '/base/ViewEvent.php', - 'yii\base\ViewRenderer' => YII_PATH . '/base/ViewRenderer.php', - 'yii\base\Widget' => YII_PATH . '/base/Widget.php', - 'yii\behaviors\AttributeBehavior' => YII_PATH . '/behaviors/AttributeBehavior.php', - 'yii\behaviors\BlameableBehavior' => YII_PATH . '/behaviors/BlameableBehavior.php', - 'yii\behaviors\TimestampBehavior' => YII_PATH . '/behaviors/TimestampBehavior.php', - 'yii\caching\ApcCache' => YII_PATH . '/caching/ApcCache.php', - 'yii\caching\Cache' => YII_PATH . '/caching/Cache.php', - 'yii\caching\ChainedDependency' => YII_PATH . '/caching/ChainedDependency.php', - 'yii\caching\DbCache' => YII_PATH . '/caching/DbCache.php', - 'yii\caching\DbDependency' => YII_PATH . '/caching/DbDependency.php', - 'yii\caching\Dependency' => YII_PATH . '/caching/Dependency.php', - 'yii\caching\DummyCache' => YII_PATH . '/caching/DummyCache.php', - 'yii\caching\ExpressionDependency' => YII_PATH . '/caching/ExpressionDependency.php', - 'yii\caching\FileCache' => YII_PATH . '/caching/FileCache.php', - 'yii\caching\FileDependency' => YII_PATH . '/caching/FileDependency.php', - 'yii\caching\GroupDependency' => YII_PATH . '/caching/GroupDependency.php', - 'yii\caching\MemCache' => YII_PATH . '/caching/MemCache.php', - 'yii\caching\MemCacheServer' => YII_PATH . '/caching/MemCacheServer.php', - 'yii\caching\WinCache' => YII_PATH . '/caching/WinCache.php', - 'yii\caching\XCache' => YII_PATH . '/caching/XCache.php', - 'yii\caching\ZendDataCache' => YII_PATH . '/caching/ZendDataCache.php', - 'yii\captcha\Captcha' => YII_PATH . '/captcha/Captcha.php', - 'yii\captcha\CaptchaAction' => YII_PATH . '/captcha/CaptchaAction.php', - 'yii\captcha\CaptchaAsset' => YII_PATH . '/captcha/CaptchaAsset.php', - 'yii\captcha\CaptchaValidator' => YII_PATH . '/captcha/CaptchaValidator.php', - 'yii\data\ActiveDataProvider' => YII_PATH . '/data/ActiveDataProvider.php', - 'yii\data\ArrayDataProvider' => YII_PATH . '/data/ArrayDataProvider.php', - 'yii\data\BaseDataProvider' => YII_PATH . '/data/BaseDataProvider.php', - 'yii\data\DataProviderInterface' => YII_PATH . '/data/DataProviderInterface.php', - 'yii\data\ModelSerializer' => YII_PATH . '/data/ModelSerializer.php', - 'yii\data\Pagination' => YII_PATH . '/data/Pagination.php', - 'yii\data\Sort' => YII_PATH . '/data/Sort.php', - 'yii\data\SqlDataProvider' => YII_PATH . '/data/SqlDataProvider.php', - 'yii\db\ActiveQuery' => YII_PATH . '/db/ActiveQuery.php', - 'yii\db\ActiveQueryInterface' => YII_PATH . '/db/ActiveQueryInterface.php', - 'yii\db\ActiveQueryTrait' => YII_PATH . '/db/ActiveQueryTrait.php', - 'yii\db\ActiveRecord' => YII_PATH . '/db/ActiveRecord.php', - 'yii\db\ActiveRecordInterface' => YII_PATH . '/db/ActiveRecordInterface.php', - 'yii\db\ActiveRelation' => YII_PATH . '/db/ActiveRelation.php', - 'yii\db\ActiveRelationInterface' => YII_PATH . '/db/ActiveRelationInterface.php', - 'yii\db\ActiveRelationTrait' => YII_PATH . '/db/ActiveRelationTrait.php', - 'yii\db\BaseActiveRecord' => YII_PATH . '/db/BaseActiveRecord.php', - 'yii\db\BatchQueryResult' => YII_PATH . '/db/BatchQueryResult.php', - 'yii\db\ColumnSchema' => YII_PATH . '/db/ColumnSchema.php', - 'yii\db\Command' => YII_PATH . '/db/Command.php', - 'yii\db\Connection' => YII_PATH . '/db/Connection.php', - 'yii\db\DataReader' => YII_PATH . '/db/DataReader.php', - 'yii\db\Exception' => YII_PATH . '/db/Exception.php', - 'yii\db\Expression' => YII_PATH . '/db/Expression.php', - 'yii\db\Migration' => YII_PATH . '/db/Migration.php', - 'yii\db\Query' => YII_PATH . '/db/Query.php', - 'yii\db\QueryBuilder' => YII_PATH . '/db/QueryBuilder.php', - 'yii\db\QueryInterface' => YII_PATH . '/db/QueryInterface.php', - 'yii\db\QueryTrait' => YII_PATH . '/db/QueryTrait.php', - 'yii\db\Schema' => YII_PATH . '/db/Schema.php', - 'yii\db\StaleObjectException' => YII_PATH . '/db/StaleObjectException.php', - 'yii\db\TableSchema' => YII_PATH . '/db/TableSchema.php', - 'yii\db\Transaction' => YII_PATH . '/db/Transaction.php', - 'yii\db\cubrid\QueryBuilder' => YII_PATH . '/db/cubrid/QueryBuilder.php', - 'yii\db\cubrid\Schema' => YII_PATH . '/db/cubrid/Schema.php', - 'yii\db\mssql\PDO' => YII_PATH . '/db/mssql/PDO.php', - 'yii\db\mssql\QueryBuilder' => YII_PATH . '/db/mssql/QueryBuilder.php', - 'yii\db\mssql\Schema' => YII_PATH . '/db/mssql/Schema.php', - 'yii\db\mssql\SqlsrvPDO' => YII_PATH . '/db/mssql/SqlsrvPDO.php', - 'yii\db\mssql\TableSchema' => YII_PATH . '/db/mssql/TableSchema.php', - 'yii\db\mysql\QueryBuilder' => YII_PATH . '/db/mysql/QueryBuilder.php', - 'yii\db\mysql\Schema' => YII_PATH . '/db/mysql/Schema.php', - 'yii\db\oci\QueryBuilder' => YII_PATH . '/db/oci/QueryBuilder.php', - 'yii\db\oci\Schema' => YII_PATH . '/db/oci/Schema.php', - 'yii\db\pgsql\QueryBuilder' => YII_PATH . '/db/pgsql/QueryBuilder.php', - 'yii\db\pgsql\Schema' => YII_PATH . '/db/pgsql/Schema.php', - 'yii\db\sqlite\QueryBuilder' => YII_PATH . '/db/sqlite/QueryBuilder.php', - 'yii\db\sqlite\Schema' => YII_PATH . '/db/sqlite/Schema.php', - 'yii\grid\ActionColumn' => YII_PATH . '/grid/ActionColumn.php', - 'yii\grid\CheckboxColumn' => YII_PATH . '/grid/CheckboxColumn.php', - 'yii\grid\Column' => YII_PATH . '/grid/Column.php', - 'yii\grid\DataColumn' => YII_PATH . '/grid/DataColumn.php', - 'yii\grid\GridView' => YII_PATH . '/grid/GridView.php', - 'yii\grid\GridViewAsset' => YII_PATH . '/grid/GridViewAsset.php', - 'yii\grid\SerialColumn' => YII_PATH . '/grid/SerialColumn.php', - 'yii\helpers\ArrayHelper' => YII_PATH . '/helpers/ArrayHelper.php', - 'yii\helpers\BaseArrayHelper' => YII_PATH . '/helpers/BaseArrayHelper.php', - 'yii\helpers\BaseConsole' => YII_PATH . '/helpers/BaseConsole.php', - 'yii\helpers\BaseFileHelper' => YII_PATH . '/helpers/BaseFileHelper.php', - 'yii\helpers\BaseHtml' => YII_PATH . '/helpers/BaseHtml.php', - 'yii\helpers\BaseHtmlPurifier' => YII_PATH . '/helpers/BaseHtmlPurifier.php', - 'yii\helpers\BaseInflector' => YII_PATH . '/helpers/BaseInflector.php', - 'yii\helpers\BaseJson' => YII_PATH . '/helpers/BaseJson.php', - 'yii\helpers\BaseMarkdown' => YII_PATH . '/helpers/BaseMarkdown.php', - 'yii\helpers\BaseSecurity' => YII_PATH . '/helpers/BaseSecurity.php', - 'yii\helpers\BaseStringHelper' => YII_PATH . '/helpers/BaseStringHelper.php', - 'yii\helpers\BaseVarDumper' => YII_PATH . '/helpers/BaseVarDumper.php', - 'yii\helpers\Console' => YII_PATH . '/helpers/Console.php', - 'yii\helpers\FileHelper' => YII_PATH . '/helpers/FileHelper.php', - 'yii\helpers\Html' => YII_PATH . '/helpers/Html.php', - 'yii\helpers\HtmlPurifier' => YII_PATH . '/helpers/HtmlPurifier.php', - 'yii\helpers\Inflector' => YII_PATH . '/helpers/Inflector.php', - 'yii\helpers\Json' => YII_PATH . '/helpers/Json.php', - 'yii\helpers\Markdown' => YII_PATH . '/helpers/Markdown.php', - 'yii\helpers\Security' => YII_PATH . '/helpers/Security.php', - 'yii\helpers\StringHelper' => YII_PATH . '/helpers/StringHelper.php', - 'yii\helpers\VarDumper' => YII_PATH . '/helpers/VarDumper.php', - 'yii\i18n\DbMessageSource' => YII_PATH . '/i18n/DbMessageSource.php', - 'yii\i18n\Formatter' => YII_PATH . '/i18n/Formatter.php', - 'yii\i18n\GettextFile' => YII_PATH . '/i18n/GettextFile.php', - 'yii\i18n\GettextMessageSource' => YII_PATH . '/i18n/GettextMessageSource.php', - 'yii\i18n\GettextMoFile' => YII_PATH . '/i18n/GettextMoFile.php', - 'yii\i18n\GettextPoFile' => YII_PATH . '/i18n/GettextPoFile.php', - 'yii\i18n\I18N' => YII_PATH . '/i18n/I18N.php', - 'yii\i18n\MessageFormatter' => YII_PATH . '/i18n/MessageFormatter.php', - 'yii\i18n\MessageSource' => YII_PATH . '/i18n/MessageSource.php', - 'yii\i18n\MissingTranslationEvent' => YII_PATH . '/i18n/MissingTranslationEvent.php', - 'yii\i18n\PhpMessageSource' => YII_PATH . '/i18n/PhpMessageSource.php', - 'yii\log\DbTarget' => YII_PATH . '/log/DbTarget.php', - 'yii\log\EmailTarget' => YII_PATH . '/log/EmailTarget.php', - 'yii\log\FileTarget' => YII_PATH . '/log/FileTarget.php', - 'yii\log\Logger' => YII_PATH . '/log/Logger.php', - 'yii\log\Target' => YII_PATH . '/log/Target.php', - 'yii\mail\BaseMailer' => YII_PATH . '/mail/BaseMailer.php', - 'yii\mail\BaseMessage' => YII_PATH . '/mail/BaseMessage.php', - 'yii\mail\MailerInterface' => YII_PATH . '/mail/MailerInterface.php', - 'yii\mail\MessageInterface' => YII_PATH . '/mail/MessageInterface.php', - 'yii\mutex\DbMutex' => YII_PATH . '/mutex/DbMutex.php', - 'yii\mutex\FileMutex' => YII_PATH . '/mutex/FileMutex.php', - 'yii\mutex\Mutex' => YII_PATH . '/mutex/Mutex.php', - 'yii\mutex\MysqlMutex' => YII_PATH . '/mutex/MysqlMutex.php', - 'yii\rbac\Assignment' => YII_PATH . '/rbac/Assignment.php', - 'yii\rbac\DbManager' => YII_PATH . '/rbac/DbManager.php', - 'yii\rbac\Item' => YII_PATH . '/rbac/Item.php', - 'yii\rbac\Manager' => YII_PATH . '/rbac/Manager.php', - 'yii\rbac\PhpManager' => YII_PATH . '/rbac/PhpManager.php', - 'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php', - 'yii\test\ActiveFixture' => YII_PATH . '/test/ActiveFixture.php', - 'yii\test\BaseActiveFixture' => YII_PATH . '/test/BaseActiveFixture.php', - 'yii\test\DbFixture' => YII_PATH . '/test/DbFixture.php', - 'yii\test\Fixture' => YII_PATH . '/test/Fixture.php', - 'yii\test\FixtureTrait' => YII_PATH . '/test/FixtureTrait.php', - 'yii\test\InitDbFixture' => YII_PATH . '/test/InitDbFixture.php', - 'yii\validators\BooleanValidator' => YII_PATH . '/validators/BooleanValidator.php', - 'yii\validators\CompareValidator' => YII_PATH . '/validators/CompareValidator.php', - 'yii\validators\DateValidator' => YII_PATH . '/validators/DateValidator.php', - 'yii\validators\DefaultValueValidator' => YII_PATH . '/validators/DefaultValueValidator.php', - 'yii\validators\EmailValidator' => YII_PATH . '/validators/EmailValidator.php', - 'yii\validators\ExistValidator' => YII_PATH . '/validators/ExistValidator.php', - 'yii\validators\FileValidator' => YII_PATH . '/validators/FileValidator.php', - 'yii\validators\FilterValidator' => YII_PATH . '/validators/FilterValidator.php', - 'yii\validators\ImageValidator' => YII_PATH . '/validators/ImageValidator.php', - 'yii\validators\InlineValidator' => YII_PATH . '/validators/InlineValidator.php', - 'yii\validators\NumberValidator' => YII_PATH . '/validators/NumberValidator.php', - 'yii\validators\PunycodeAsset' => YII_PATH . '/validators/PunycodeAsset.php', - 'yii\validators\RangeValidator' => YII_PATH . '/validators/RangeValidator.php', - 'yii\validators\RegularExpressionValidator' => YII_PATH . '/validators/RegularExpressionValidator.php', - 'yii\validators\RequiredValidator' => YII_PATH . '/validators/RequiredValidator.php', - 'yii\validators\SafeValidator' => YII_PATH . '/validators/SafeValidator.php', - 'yii\validators\StringValidator' => YII_PATH . '/validators/StringValidator.php', - 'yii\validators\UniqueValidator' => YII_PATH . '/validators/UniqueValidator.php', - 'yii\validators\UrlValidator' => YII_PATH . '/validators/UrlValidator.php', - 'yii\validators\ValidationAsset' => YII_PATH . '/validators/ValidationAsset.php', - 'yii\validators\Validator' => YII_PATH . '/validators/Validator.php', - 'yii\web\AccessControl' => YII_PATH . '/web/AccessControl.php', - 'yii\web\AccessRule' => YII_PATH . '/web/AccessRule.php', - 'yii\web\Application' => YII_PATH . '/web/Application.php', - 'yii\web\AssetBundle' => YII_PATH . '/web/AssetBundle.php', - 'yii\web\AssetConverter' => YII_PATH . '/web/AssetConverter.php', - 'yii\web\AssetConverterInterface' => YII_PATH . '/web/AssetConverterInterface.php', - 'yii\web\AssetManager' => YII_PATH . '/web/AssetManager.php', - 'yii\web\BadRequestHttpException' => YII_PATH . '/web/BadRequestHttpException.php', - 'yii\web\CacheSession' => YII_PATH . '/web/CacheSession.php', - 'yii\web\ConflictHttpException' => YII_PATH . '/web/ConflictHttpException.php', - 'yii\web\Controller' => YII_PATH . '/web/Controller.php', - 'yii\web\Cookie' => YII_PATH . '/web/Cookie.php', - 'yii\web\CookieCollection' => YII_PATH . '/web/CookieCollection.php', - 'yii\web\DbSession' => YII_PATH . '/web/DbSession.php', - 'yii\web\ErrorAction' => YII_PATH . '/web/ErrorAction.php', - 'yii\web\ForbiddenHttpException' => YII_PATH . '/web/ForbiddenHttpException.php', - 'yii\web\GoneHttpException' => YII_PATH . '/web/GoneHttpException.php', - 'yii\web\HeaderCollection' => YII_PATH . '/web/HeaderCollection.php', - 'yii\web\HttpCache' => YII_PATH . '/web/HttpCache.php', - 'yii\web\HttpException' => YII_PATH . '/web/HttpException.php', - 'yii\web\IdentityInterface' => YII_PATH . '/web/IdentityInterface.php', - 'yii\web\JqueryAsset' => YII_PATH . '/web/JqueryAsset.php', - 'yii\web\JsExpression' => YII_PATH . '/web/JsExpression.php', - 'yii\web\JsonParser' => YII_PATH . '/web/JsonParser.php', - 'yii\web\MethodNotAllowedHttpException' => YII_PATH . '/web/MethodNotAllowedHttpException.php', - 'yii\web\NotAcceptableHttpException' => YII_PATH . '/web/NotAcceptableHttpException.php', - 'yii\web\NotFoundHttpException' => YII_PATH . '/web/NotFoundHttpException.php', - 'yii\web\PageCache' => YII_PATH . '/web/PageCache.php', - 'yii\web\Request' => YII_PATH . '/web/Request.php', - 'yii\web\RequestParserInterface' => YII_PATH . '/web/RequestParserInterface.php', - 'yii\web\Response' => YII_PATH . '/web/Response.php', - 'yii\web\ResponseFormatterInterface' => YII_PATH . '/web/ResponseFormatterInterface.php', - 'yii\web\Session' => YII_PATH . '/web/Session.php', - 'yii\web\SessionIterator' => YII_PATH . '/web/SessionIterator.php', - 'yii\web\TooManyRequestsHttpException' => YII_PATH . '/web/TooManyRequestsHttpException.php', - 'yii\web\UnauthorizedHttpException' => YII_PATH . '/web/UnauthorizedHttpException.php', - 'yii\web\UnsupportedMediaTypeHttpException' => YII_PATH . '/web/UnsupportedMediaTypeHttpException.php', - 'yii\web\UploadedFile' => YII_PATH . '/web/UploadedFile.php', - 'yii\web\UrlManager' => YII_PATH . '/web/UrlManager.php', - 'yii\web\UrlRule' => YII_PATH . '/web/UrlRule.php', - 'yii\web\User' => YII_PATH . '/web/User.php', - 'yii\web\UserEvent' => YII_PATH . '/web/UserEvent.php', - 'yii\web\VerbFilter' => YII_PATH . '/web/VerbFilter.php', - 'yii\web\View' => YII_PATH . '/web/View.php', - 'yii\web\XmlResponseFormatter' => YII_PATH . '/web/XmlResponseFormatter.php', - 'yii\web\YiiAsset' => YII_PATH . '/web/YiiAsset.php', - 'yii\widgets\ActiveField' => YII_PATH . '/widgets/ActiveField.php', - 'yii\widgets\ActiveForm' => YII_PATH . '/widgets/ActiveForm.php', - 'yii\widgets\ActiveFormAsset' => YII_PATH . '/widgets/ActiveFormAsset.php', - 'yii\widgets\BaseListView' => YII_PATH . '/widgets/BaseListView.php', - 'yii\widgets\Block' => YII_PATH . '/widgets/Block.php', - 'yii\widgets\Breadcrumbs' => YII_PATH . '/widgets/Breadcrumbs.php', - 'yii\widgets\ContentDecorator' => YII_PATH . '/widgets/ContentDecorator.php', - 'yii\widgets\DetailView' => YII_PATH . '/widgets/DetailView.php', - 'yii\widgets\FragmentCache' => YII_PATH . '/widgets/FragmentCache.php', - 'yii\widgets\InputWidget' => YII_PATH . '/widgets/InputWidget.php', - 'yii\widgets\LinkPager' => YII_PATH . '/widgets/LinkPager.php', - 'yii\widgets\LinkSorter' => YII_PATH . '/widgets/LinkSorter.php', - 'yii\widgets\ListView' => YII_PATH . '/widgets/ListView.php', - 'yii\widgets\MaskedInput' => YII_PATH . '/widgets/MaskedInput.php', - 'yii\widgets\MaskedInputAsset' => YII_PATH . '/widgets/MaskedInputAsset.php', - 'yii\widgets\Menu' => YII_PATH . '/widgets/Menu.php', - 'yii\widgets\Pjax' => YII_PATH . '/widgets/Pjax.php', - 'yii\widgets\PjaxAsset' => YII_PATH . '/widgets/PjaxAsset.php', - 'yii\widgets\Spaceless' => YII_PATH . '/widgets/Spaceless.php', + 'yii\base\Action' => YII_PATH . '/base/Action.php', + 'yii\base\ActionEvent' => YII_PATH . '/base/ActionEvent.php', + 'yii\base\ActionFilter' => YII_PATH . '/base/ActionFilter.php', + 'yii\base\Application' => YII_PATH . '/base/Application.php', + 'yii\base\ArrayAccessTrait' => YII_PATH . '/base/ArrayAccessTrait.php', + 'yii\base\Arrayable' => YII_PATH . '/base/Arrayable.php', + 'yii\base\Behavior' => YII_PATH . '/base/Behavior.php', + 'yii\base\Component' => YII_PATH . '/base/Component.php', + 'yii\base\Controller' => YII_PATH . '/base/Controller.php', + 'yii\base\DynamicModel' => YII_PATH . '/base/DynamicModel.php', + 'yii\base\ErrorException' => YII_PATH . '/base/ErrorException.php', + 'yii\base\ErrorHandler' => YII_PATH . '/base/ErrorHandler.php', + 'yii\base\Event' => YII_PATH . '/base/Event.php', + 'yii\base\Exception' => YII_PATH . '/base/Exception.php', + 'yii\base\Extension' => YII_PATH . '/base/Extension.php', + 'yii\base\Formatter' => YII_PATH . '/base/Formatter.php', + 'yii\base\InlineAction' => YII_PATH . '/base/InlineAction.php', + 'yii\base\InvalidCallException' => YII_PATH . '/base/InvalidCallException.php', + 'yii\base\InvalidConfigException' => YII_PATH . '/base/InvalidConfigException.php', + 'yii\base\InvalidParamException' => YII_PATH . '/base/InvalidParamException.php', + 'yii\base\InvalidRouteException' => YII_PATH . '/base/InvalidRouteException.php', + 'yii\base\MailEvent' => YII_PATH . '/base/MailEvent.php', + 'yii\base\Model' => YII_PATH . '/base/Model.php', + 'yii\base\ModelEvent' => YII_PATH . '/base/ModelEvent.php', + 'yii\base\Module' => YII_PATH . '/base/Module.php', + 'yii\base\NotSupportedException' => YII_PATH . '/base/NotSupportedException.php', + 'yii\base\Object' => YII_PATH . '/base/Object.php', + 'yii\base\Request' => YII_PATH . '/base/Request.php', + 'yii\base\Response' => YII_PATH . '/base/Response.php', + 'yii\base\Theme' => YII_PATH . '/base/Theme.php', + 'yii\base\UnknownClassException' => YII_PATH . '/base/UnknownClassException.php', + 'yii\base\UnknownMethodException' => YII_PATH . '/base/UnknownMethodException.php', + 'yii\base\UnknownPropertyException' => YII_PATH . '/base/UnknownPropertyException.php', + 'yii\base\UserException' => YII_PATH . '/base/UserException.php', + 'yii\base\View' => YII_PATH . '/base/View.php', + 'yii\base\ViewContextInterface' => YII_PATH . '/base/ViewContextInterface.php', + 'yii\base\ViewEvent' => YII_PATH . '/base/ViewEvent.php', + 'yii\base\ViewRenderer' => YII_PATH . '/base/ViewRenderer.php', + 'yii\base\Widget' => YII_PATH . '/base/Widget.php', + 'yii\behaviors\AttributeBehavior' => YII_PATH . '/behaviors/AttributeBehavior.php', + 'yii\behaviors\BlameableBehavior' => YII_PATH . '/behaviors/BlameableBehavior.php', + 'yii\behaviors\TimestampBehavior' => YII_PATH . '/behaviors/TimestampBehavior.php', + 'yii\caching\ApcCache' => YII_PATH . '/caching/ApcCache.php', + 'yii\caching\Cache' => YII_PATH . '/caching/Cache.php', + 'yii\caching\ChainedDependency' => YII_PATH . '/caching/ChainedDependency.php', + 'yii\caching\DbCache' => YII_PATH . '/caching/DbCache.php', + 'yii\caching\DbDependency' => YII_PATH . '/caching/DbDependency.php', + 'yii\caching\Dependency' => YII_PATH . '/caching/Dependency.php', + 'yii\caching\DummyCache' => YII_PATH . '/caching/DummyCache.php', + 'yii\caching\ExpressionDependency' => YII_PATH . '/caching/ExpressionDependency.php', + 'yii\caching\FileCache' => YII_PATH . '/caching/FileCache.php', + 'yii\caching\FileDependency' => YII_PATH . '/caching/FileDependency.php', + 'yii\caching\GroupDependency' => YII_PATH . '/caching/GroupDependency.php', + 'yii\caching\MemCache' => YII_PATH . '/caching/MemCache.php', + 'yii\caching\MemCacheServer' => YII_PATH . '/caching/MemCacheServer.php', + 'yii\caching\WinCache' => YII_PATH . '/caching/WinCache.php', + 'yii\caching\XCache' => YII_PATH . '/caching/XCache.php', + 'yii\caching\ZendDataCache' => YII_PATH . '/caching/ZendDataCache.php', + 'yii\captcha\Captcha' => YII_PATH . '/captcha/Captcha.php', + 'yii\captcha\CaptchaAction' => YII_PATH . '/captcha/CaptchaAction.php', + 'yii\captcha\CaptchaAsset' => YII_PATH . '/captcha/CaptchaAsset.php', + 'yii\captcha\CaptchaValidator' => YII_PATH . '/captcha/CaptchaValidator.php', + 'yii\data\ActiveDataProvider' => YII_PATH . '/data/ActiveDataProvider.php', + 'yii\data\ArrayDataProvider' => YII_PATH . '/data/ArrayDataProvider.php', + 'yii\data\BaseDataProvider' => YII_PATH . '/data/BaseDataProvider.php', + 'yii\data\DataProviderInterface' => YII_PATH . '/data/DataProviderInterface.php', + 'yii\data\ModelSerializer' => YII_PATH . '/data/ModelSerializer.php', + 'yii\data\Pagination' => YII_PATH . '/data/Pagination.php', + 'yii\data\Sort' => YII_PATH . '/data/Sort.php', + 'yii\data\SqlDataProvider' => YII_PATH . '/data/SqlDataProvider.php', + 'yii\db\ActiveQuery' => YII_PATH . '/db/ActiveQuery.php', + 'yii\db\ActiveQueryInterface' => YII_PATH . '/db/ActiveQueryInterface.php', + 'yii\db\ActiveQueryTrait' => YII_PATH . '/db/ActiveQueryTrait.php', + 'yii\db\ActiveRecord' => YII_PATH . '/db/ActiveRecord.php', + 'yii\db\ActiveRecordInterface' => YII_PATH . '/db/ActiveRecordInterface.php', + 'yii\db\ActiveRelation' => YII_PATH . '/db/ActiveRelation.php', + 'yii\db\ActiveRelationInterface' => YII_PATH . '/db/ActiveRelationInterface.php', + 'yii\db\ActiveRelationTrait' => YII_PATH . '/db/ActiveRelationTrait.php', + 'yii\db\BaseActiveRecord' => YII_PATH . '/db/BaseActiveRecord.php', + 'yii\db\BatchQueryResult' => YII_PATH . '/db/BatchQueryResult.php', + 'yii\db\ColumnSchema' => YII_PATH . '/db/ColumnSchema.php', + 'yii\db\Command' => YII_PATH . '/db/Command.php', + 'yii\db\Connection' => YII_PATH . '/db/Connection.php', + 'yii\db\DataReader' => YII_PATH . '/db/DataReader.php', + 'yii\db\Exception' => YII_PATH . '/db/Exception.php', + 'yii\db\Expression' => YII_PATH . '/db/Expression.php', + 'yii\db\Migration' => YII_PATH . '/db/Migration.php', + 'yii\db\Query' => YII_PATH . '/db/Query.php', + 'yii\db\QueryBuilder' => YII_PATH . '/db/QueryBuilder.php', + 'yii\db\QueryInterface' => YII_PATH . '/db/QueryInterface.php', + 'yii\db\QueryTrait' => YII_PATH . '/db/QueryTrait.php', + 'yii\db\Schema' => YII_PATH . '/db/Schema.php', + 'yii\db\StaleObjectException' => YII_PATH . '/db/StaleObjectException.php', + 'yii\db\TableSchema' => YII_PATH . '/db/TableSchema.php', + 'yii\db\Transaction' => YII_PATH . '/db/Transaction.php', + 'yii\db\cubrid\QueryBuilder' => YII_PATH . '/db/cubrid/QueryBuilder.php', + 'yii\db\cubrid\Schema' => YII_PATH . '/db/cubrid/Schema.php', + 'yii\db\mssql\PDO' => YII_PATH . '/db/mssql/PDO.php', + 'yii\db\mssql\QueryBuilder' => YII_PATH . '/db/mssql/QueryBuilder.php', + 'yii\db\mssql\Schema' => YII_PATH . '/db/mssql/Schema.php', + 'yii\db\mssql\SqlsrvPDO' => YII_PATH . '/db/mssql/SqlsrvPDO.php', + 'yii\db\mssql\TableSchema' => YII_PATH . '/db/mssql/TableSchema.php', + 'yii\db\mysql\QueryBuilder' => YII_PATH . '/db/mysql/QueryBuilder.php', + 'yii\db\mysql\Schema' => YII_PATH . '/db/mysql/Schema.php', + 'yii\db\oci\QueryBuilder' => YII_PATH . '/db/oci/QueryBuilder.php', + 'yii\db\oci\Schema' => YII_PATH . '/db/oci/Schema.php', + 'yii\db\pgsql\QueryBuilder' => YII_PATH . '/db/pgsql/QueryBuilder.php', + 'yii\db\pgsql\Schema' => YII_PATH . '/db/pgsql/Schema.php', + 'yii\db\sqlite\QueryBuilder' => YII_PATH . '/db/sqlite/QueryBuilder.php', + 'yii\db\sqlite\Schema' => YII_PATH . '/db/sqlite/Schema.php', + 'yii\grid\ActionColumn' => YII_PATH . '/grid/ActionColumn.php', + 'yii\grid\CheckboxColumn' => YII_PATH . '/grid/CheckboxColumn.php', + 'yii\grid\Column' => YII_PATH . '/grid/Column.php', + 'yii\grid\DataColumn' => YII_PATH . '/grid/DataColumn.php', + 'yii\grid\GridView' => YII_PATH . '/grid/GridView.php', + 'yii\grid\GridViewAsset' => YII_PATH . '/grid/GridViewAsset.php', + 'yii\grid\SerialColumn' => YII_PATH . '/grid/SerialColumn.php', + 'yii\helpers\ArrayHelper' => YII_PATH . '/helpers/ArrayHelper.php', + 'yii\helpers\BaseArrayHelper' => YII_PATH . '/helpers/BaseArrayHelper.php', + 'yii\helpers\BaseConsole' => YII_PATH . '/helpers/BaseConsole.php', + 'yii\helpers\BaseFileHelper' => YII_PATH . '/helpers/BaseFileHelper.php', + 'yii\helpers\BaseHtml' => YII_PATH . '/helpers/BaseHtml.php', + 'yii\helpers\BaseHtmlPurifier' => YII_PATH . '/helpers/BaseHtmlPurifier.php', + 'yii\helpers\BaseInflector' => YII_PATH . '/helpers/BaseInflector.php', + 'yii\helpers\BaseJson' => YII_PATH . '/helpers/BaseJson.php', + 'yii\helpers\BaseMarkdown' => YII_PATH . '/helpers/BaseMarkdown.php', + 'yii\helpers\BaseSecurity' => YII_PATH . '/helpers/BaseSecurity.php', + 'yii\helpers\BaseStringHelper' => YII_PATH . '/helpers/BaseStringHelper.php', + 'yii\helpers\BaseVarDumper' => YII_PATH . '/helpers/BaseVarDumper.php', + 'yii\helpers\Console' => YII_PATH . '/helpers/Console.php', + 'yii\helpers\FileHelper' => YII_PATH . '/helpers/FileHelper.php', + 'yii\helpers\Html' => YII_PATH . '/helpers/Html.php', + 'yii\helpers\HtmlPurifier' => YII_PATH . '/helpers/HtmlPurifier.php', + 'yii\helpers\Inflector' => YII_PATH . '/helpers/Inflector.php', + 'yii\helpers\Json' => YII_PATH . '/helpers/Json.php', + 'yii\helpers\Markdown' => YII_PATH . '/helpers/Markdown.php', + 'yii\helpers\Security' => YII_PATH . '/helpers/Security.php', + 'yii\helpers\StringHelper' => YII_PATH . '/helpers/StringHelper.php', + 'yii\helpers\VarDumper' => YII_PATH . '/helpers/VarDumper.php', + 'yii\i18n\DbMessageSource' => YII_PATH . '/i18n/DbMessageSource.php', + 'yii\i18n\Formatter' => YII_PATH . '/i18n/Formatter.php', + 'yii\i18n\GettextFile' => YII_PATH . '/i18n/GettextFile.php', + 'yii\i18n\GettextMessageSource' => YII_PATH . '/i18n/GettextMessageSource.php', + 'yii\i18n\GettextMoFile' => YII_PATH . '/i18n/GettextMoFile.php', + 'yii\i18n\GettextPoFile' => YII_PATH . '/i18n/GettextPoFile.php', + 'yii\i18n\I18N' => YII_PATH . '/i18n/I18N.php', + 'yii\i18n\MessageFormatter' => YII_PATH . '/i18n/MessageFormatter.php', + 'yii\i18n\MessageSource' => YII_PATH . '/i18n/MessageSource.php', + 'yii\i18n\MissingTranslationEvent' => YII_PATH . '/i18n/MissingTranslationEvent.php', + 'yii\i18n\PhpMessageSource' => YII_PATH . '/i18n/PhpMessageSource.php', + 'yii\log\DbTarget' => YII_PATH . '/log/DbTarget.php', + 'yii\log\EmailTarget' => YII_PATH . '/log/EmailTarget.php', + 'yii\log\FileTarget' => YII_PATH . '/log/FileTarget.php', + 'yii\log\Logger' => YII_PATH . '/log/Logger.php', + 'yii\log\Target' => YII_PATH . '/log/Target.php', + 'yii\mail\BaseMailer' => YII_PATH . '/mail/BaseMailer.php', + 'yii\mail\BaseMessage' => YII_PATH . '/mail/BaseMessage.php', + 'yii\mail\MailerInterface' => YII_PATH . '/mail/MailerInterface.php', + 'yii\mail\MessageInterface' => YII_PATH . '/mail/MessageInterface.php', + 'yii\mutex\DbMutex' => YII_PATH . '/mutex/DbMutex.php', + 'yii\mutex\FileMutex' => YII_PATH . '/mutex/FileMutex.php', + 'yii\mutex\Mutex' => YII_PATH . '/mutex/Mutex.php', + 'yii\mutex\MysqlMutex' => YII_PATH . '/mutex/MysqlMutex.php', + 'yii\rbac\Assignment' => YII_PATH . '/rbac/Assignment.php', + 'yii\rbac\DbManager' => YII_PATH . '/rbac/DbManager.php', + 'yii\rbac\Item' => YII_PATH . '/rbac/Item.php', + 'yii\rbac\Manager' => YII_PATH . '/rbac/Manager.php', + 'yii\rbac\PhpManager' => YII_PATH . '/rbac/PhpManager.php', + 'yii\requirements\YiiRequirementChecker' => YII_PATH . '/requirements/YiiRequirementChecker.php', + 'yii\test\ActiveFixture' => YII_PATH . '/test/ActiveFixture.php', + 'yii\test\BaseActiveFixture' => YII_PATH . '/test/BaseActiveFixture.php', + 'yii\test\DbFixture' => YII_PATH . '/test/DbFixture.php', + 'yii\test\Fixture' => YII_PATH . '/test/Fixture.php', + 'yii\test\FixtureTrait' => YII_PATH . '/test/FixtureTrait.php', + 'yii\test\InitDbFixture' => YII_PATH . '/test/InitDbFixture.php', + 'yii\validators\BooleanValidator' => YII_PATH . '/validators/BooleanValidator.php', + 'yii\validators\CompareValidator' => YII_PATH . '/validators/CompareValidator.php', + 'yii\validators\DateValidator' => YII_PATH . '/validators/DateValidator.php', + 'yii\validators\DefaultValueValidator' => YII_PATH . '/validators/DefaultValueValidator.php', + 'yii\validators\EmailValidator' => YII_PATH . '/validators/EmailValidator.php', + 'yii\validators\ExistValidator' => YII_PATH . '/validators/ExistValidator.php', + 'yii\validators\FileValidator' => YII_PATH . '/validators/FileValidator.php', + 'yii\validators\FilterValidator' => YII_PATH . '/validators/FilterValidator.php', + 'yii\validators\ImageValidator' => YII_PATH . '/validators/ImageValidator.php', + 'yii\validators\InlineValidator' => YII_PATH . '/validators/InlineValidator.php', + 'yii\validators\NumberValidator' => YII_PATH . '/validators/NumberValidator.php', + 'yii\validators\PunycodeAsset' => YII_PATH . '/validators/PunycodeAsset.php', + 'yii\validators\RangeValidator' => YII_PATH . '/validators/RangeValidator.php', + 'yii\validators\RegularExpressionValidator' => YII_PATH . '/validators/RegularExpressionValidator.php', + 'yii\validators\RequiredValidator' => YII_PATH . '/validators/RequiredValidator.php', + 'yii\validators\SafeValidator' => YII_PATH . '/validators/SafeValidator.php', + 'yii\validators\StringValidator' => YII_PATH . '/validators/StringValidator.php', + 'yii\validators\UniqueValidator' => YII_PATH . '/validators/UniqueValidator.php', + 'yii\validators\UrlValidator' => YII_PATH . '/validators/UrlValidator.php', + 'yii\validators\ValidationAsset' => YII_PATH . '/validators/ValidationAsset.php', + 'yii\validators\Validator' => YII_PATH . '/validators/Validator.php', + 'yii\web\AccessControl' => YII_PATH . '/web/AccessControl.php', + 'yii\web\AccessRule' => YII_PATH . '/web/AccessRule.php', + 'yii\web\Application' => YII_PATH . '/web/Application.php', + 'yii\web\AssetBundle' => YII_PATH . '/web/AssetBundle.php', + 'yii\web\AssetConverter' => YII_PATH . '/web/AssetConverter.php', + 'yii\web\AssetConverterInterface' => YII_PATH . '/web/AssetConverterInterface.php', + 'yii\web\AssetManager' => YII_PATH . '/web/AssetManager.php', + 'yii\web\BadRequestHttpException' => YII_PATH . '/web/BadRequestHttpException.php', + 'yii\web\CacheSession' => YII_PATH . '/web/CacheSession.php', + 'yii\web\ConflictHttpException' => YII_PATH . '/web/ConflictHttpException.php', + 'yii\web\Controller' => YII_PATH . '/web/Controller.php', + 'yii\web\Cookie' => YII_PATH . '/web/Cookie.php', + 'yii\web\CookieCollection' => YII_PATH . '/web/CookieCollection.php', + 'yii\web\DbSession' => YII_PATH . '/web/DbSession.php', + 'yii\web\ErrorAction' => YII_PATH . '/web/ErrorAction.php', + 'yii\web\ForbiddenHttpException' => YII_PATH . '/web/ForbiddenHttpException.php', + 'yii\web\GoneHttpException' => YII_PATH . '/web/GoneHttpException.php', + 'yii\web\HeaderCollection' => YII_PATH . '/web/HeaderCollection.php', + 'yii\web\HttpCache' => YII_PATH . '/web/HttpCache.php', + 'yii\web\HttpException' => YII_PATH . '/web/HttpException.php', + 'yii\web\IdentityInterface' => YII_PATH . '/web/IdentityInterface.php', + 'yii\web\JqueryAsset' => YII_PATH . '/web/JqueryAsset.php', + 'yii\web\JsExpression' => YII_PATH . '/web/JsExpression.php', + 'yii\web\JsonParser' => YII_PATH . '/web/JsonParser.php', + 'yii\web\MethodNotAllowedHttpException' => YII_PATH . '/web/MethodNotAllowedHttpException.php', + 'yii\web\NotAcceptableHttpException' => YII_PATH . '/web/NotAcceptableHttpException.php', + 'yii\web\NotFoundHttpException' => YII_PATH . '/web/NotFoundHttpException.php', + 'yii\web\PageCache' => YII_PATH . '/web/PageCache.php', + 'yii\web\Request' => YII_PATH . '/web/Request.php', + 'yii\web\RequestParserInterface' => YII_PATH . '/web/RequestParserInterface.php', + 'yii\web\Response' => YII_PATH . '/web/Response.php', + 'yii\web\ResponseFormatterInterface' => YII_PATH . '/web/ResponseFormatterInterface.php', + 'yii\web\Session' => YII_PATH . '/web/Session.php', + 'yii\web\SessionIterator' => YII_PATH . '/web/SessionIterator.php', + 'yii\web\TooManyRequestsHttpException' => YII_PATH . '/web/TooManyRequestsHttpException.php', + 'yii\web\UnauthorizedHttpException' => YII_PATH . '/web/UnauthorizedHttpException.php', + 'yii\web\UnsupportedMediaTypeHttpException' => YII_PATH . '/web/UnsupportedMediaTypeHttpException.php', + 'yii\web\UploadedFile' => YII_PATH . '/web/UploadedFile.php', + 'yii\web\UrlManager' => YII_PATH . '/web/UrlManager.php', + 'yii\web\UrlRule' => YII_PATH . '/web/UrlRule.php', + 'yii\web\User' => YII_PATH . '/web/User.php', + 'yii\web\UserEvent' => YII_PATH . '/web/UserEvent.php', + 'yii\web\VerbFilter' => YII_PATH . '/web/VerbFilter.php', + 'yii\web\View' => YII_PATH . '/web/View.php', + 'yii\web\XmlResponseFormatter' => YII_PATH . '/web/XmlResponseFormatter.php', + 'yii\web\YiiAsset' => YII_PATH . '/web/YiiAsset.php', + 'yii\widgets\ActiveField' => YII_PATH . '/widgets/ActiveField.php', + 'yii\widgets\ActiveForm' => YII_PATH . '/widgets/ActiveForm.php', + 'yii\widgets\ActiveFormAsset' => YII_PATH . '/widgets/ActiveFormAsset.php', + 'yii\widgets\BaseListView' => YII_PATH . '/widgets/BaseListView.php', + 'yii\widgets\Block' => YII_PATH . '/widgets/Block.php', + 'yii\widgets\Breadcrumbs' => YII_PATH . '/widgets/Breadcrumbs.php', + 'yii\widgets\ContentDecorator' => YII_PATH . '/widgets/ContentDecorator.php', + 'yii\widgets\DetailView' => YII_PATH . '/widgets/DetailView.php', + 'yii\widgets\FragmentCache' => YII_PATH . '/widgets/FragmentCache.php', + 'yii\widgets\InputWidget' => YII_PATH . '/widgets/InputWidget.php', + 'yii\widgets\LinkPager' => YII_PATH . '/widgets/LinkPager.php', + 'yii\widgets\LinkSorter' => YII_PATH . '/widgets/LinkSorter.php', + 'yii\widgets\ListView' => YII_PATH . '/widgets/ListView.php', + 'yii\widgets\MaskedInput' => YII_PATH . '/widgets/MaskedInput.php', + 'yii\widgets\MaskedInputAsset' => YII_PATH . '/widgets/MaskedInputAsset.php', + 'yii\widgets\Menu' => YII_PATH . '/widgets/Menu.php', + 'yii\widgets\Pjax' => YII_PATH . '/widgets/Pjax.php', + 'yii\widgets\PjaxAsset' => YII_PATH . '/widgets/PjaxAsset.php', + 'yii\widgets\Spaceless' => YII_PATH . '/widgets/Spaceless.php', ]; diff --git a/framework/console/Application.php b/framework/console/Application.php index 480311903cf..0a6b79363b8 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -53,154 +53,155 @@ */ class Application extends \yii\base\Application { - /** - * The option name for specifying the application configuration file path. - */ - const OPTION_APPCONFIG = 'appconfig'; + /** + * The option name for specifying the application configuration file path. + */ + const OPTION_APPCONFIG = 'appconfig'; - /** - * @var string the default route of this application. Defaults to 'help', - * meaning the `help` command. - */ - public $defaultRoute = 'help'; - /** - * @var boolean whether to enable the commands provided by the core framework. - * Defaults to true. - */ - public $enableCoreCommands = true; - /** - * @var Controller the currently active controller instance - */ - public $controller; + /** + * @var string the default route of this application. Defaults to 'help', + * meaning the `help` command. + */ + public $defaultRoute = 'help'; + /** + * @var boolean whether to enable the commands provided by the core framework. + * Defaults to true. + */ + public $enableCoreCommands = true; + /** + * @var Controller the currently active controller instance + */ + public $controller; + /** + * @inheritdoc + */ + public function __construct($config = []) + { + $config = $this->loadConfig($config); + parent::__construct($config); + } - /** - * @inheritdoc - */ - public function __construct($config = []) - { - $config = $this->loadConfig($config); - parent::__construct($config); - } + /** + * Loads the configuration. + * This method will check if the command line option [[OPTION_APPCONFIG]] is specified. + * If so, the corresponding file will be loaded as the application configuration. + * Otherwise, the configuration provided as the parameter will be returned back. + * @param array $config the configuration provided in the constructor. + * @return array the actual configuration to be used by the application. + */ + protected function loadConfig($config) + { + if (!empty($_SERVER['argv'])) { + $option = '--' . self::OPTION_APPCONFIG . '='; + foreach ($_SERVER['argv'] as $param) { + if (strpos($param, $option) !== false) { + $path = substr($param, strlen($option)); + if (!empty($path) && is_file($file = Yii::getAlias($path))) { + return require($file); + } else { + die("The configuration file does not exist: $path\n"); + } + } + } + } - /** - * Loads the configuration. - * This method will check if the command line option [[OPTION_APPCONFIG]] is specified. - * If so, the corresponding file will be loaded as the application configuration. - * Otherwise, the configuration provided as the parameter will be returned back. - * @param array $config the configuration provided in the constructor. - * @return array the actual configuration to be used by the application. - */ - protected function loadConfig($config) - { - if (!empty($_SERVER['argv'])) { - $option = '--' . self::OPTION_APPCONFIG . '='; - foreach ($_SERVER['argv'] as $param) { - if (strpos($param, $option) !== false) { - $path = substr($param, strlen($option)); - if (!empty($path) && is_file($file = Yii::getAlias($path))) { - return require($file); - } else { - die("The configuration file does not exist: $path\n"); - } - } - } - } - return $config; - } + return $config; + } - /** - * Initialize the application. - */ - public function init() - { - parent::init(); - if ($this->enableCoreCommands) { - foreach ($this->coreCommands() as $id => $command) { - if (!isset($this->controllerMap[$id])) { - $this->controllerMap[$id] = $command; - } - } - } - // ensure we have the 'help' command so that we can list the available commands - if (!isset($this->controllerMap['help'])) { - $this->controllerMap['help'] = 'yii\console\controllers\HelpController'; - } - } + /** + * Initialize the application. + */ + public function init() + { + parent::init(); + if ($this->enableCoreCommands) { + foreach ($this->coreCommands() as $id => $command) { + if (!isset($this->controllerMap[$id])) { + $this->controllerMap[$id] = $command; + } + } + } + // ensure we have the 'help' command so that we can list the available commands + if (!isset($this->controllerMap['help'])) { + $this->controllerMap['help'] = 'yii\console\controllers\HelpController'; + } + } - /** - * Handles the specified request. - * @param Request $request the request to be handled - * @return Response the resulting response - */ - public function handleRequest($request) - { - list ($route, $params) = $request->resolve(); - $this->requestedRoute = $route; - $result = $this->runAction($route, $params); - if ($result instanceof Response) { - return $result; - } else { - $response = $this->getResponse(); - $response->exitStatus = (int)$result; - return $response; - } - } + /** + * Handles the specified request. + * @param Request $request the request to be handled + * @return Response the resulting response + */ + public function handleRequest($request) + { + list ($route, $params) = $request->resolve(); + $this->requestedRoute = $route; + $result = $this->runAction($route, $params); + if ($result instanceof Response) { + return $result; + } else { + $response = $this->getResponse(); + $response->exitStatus = (int) $result; - /** - * Returns the response component. - * @return Response the response component - */ - public function getResponse() - { - return $this->getComponent('response'); - } + return $response; + } + } - /** - * Runs a controller action specified by a route. - * This method parses the specified route and creates the corresponding child module(s), controller and action - * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. - * If the route is empty, the method will use [[defaultRoute]]. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. - * @throws Exception if the route is invalid - */ - public function runAction($route, $params = []) - { - try { - return parent::runAction($route, $params); - } catch (InvalidRouteException $e) { - throw new Exception(Yii::t('yii', 'Unknown command "{command}".', ['command' => $route]), 0, $e); - } - } + /** + * Returns the response component. + * @return Response the response component + */ + public function getResponse() + { + return $this->getComponent('response'); + } - /** - * Returns the configuration of the built-in commands. - * @return array the configuration of the built-in commands. - */ - public function coreCommands() - { - return [ - 'message' => 'yii\console\controllers\MessageController', - 'help' => 'yii\console\controllers\HelpController', - 'migrate' => 'yii\console\controllers\MigrateController', - 'cache' => 'yii\console\controllers\CacheController', - 'asset' => 'yii\console\controllers\AssetController', - 'fixture' => 'yii\console\controllers\FixtureController', - ]; - } + /** + * Runs a controller action specified by a route. + * This method parses the specified route and creates the corresponding child module(s), controller and action + * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. + * If the route is empty, the method will use [[defaultRoute]]. + * @param string $route the route that specifies the action. + * @param array $params the parameters to be passed to the action + * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @throws Exception if the route is invalid + */ + public function runAction($route, $params = []) + { + try { + return parent::runAction($route, $params); + } catch (InvalidRouteException $e) { + throw new Exception(Yii::t('yii', 'Unknown command "{command}".', ['command' => $route]), 0, $e); + } + } - /** - * Registers the core application components. - * @see setComponents - */ - public function registerCoreComponents() - { - parent::registerCoreComponents(); - $this->setComponents([ - 'request' => ['class' => 'yii\console\Request'], - 'response' => ['class' => 'yii\console\Response'], - ]); - } + /** + * Returns the configuration of the built-in commands. + * @return array the configuration of the built-in commands. + */ + public function coreCommands() + { + return [ + 'message' => 'yii\console\controllers\MessageController', + 'help' => 'yii\console\controllers\HelpController', + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + 'asset' => 'yii\console\controllers\AssetController', + 'fixture' => 'yii\console\controllers\FixtureController', + ]; + } + + /** + * Registers the core application components. + * @see setComponents + */ + public function registerCoreComponents() + { + parent::registerCoreComponents(); + $this->setComponents([ + 'request' => ['class' => 'yii\console\Request'], + 'response' => ['class' => 'yii\console\Response'], + ]); + } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 779b6615213..7f817bf5172 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -29,244 +29,247 @@ */ class Controller extends \yii\base\Controller { - /** - * @var boolean whether to run the command interactively. - */ - public $interactive = true; + /** + * @var boolean whether to run the command interactively. + */ + public $interactive = true; - /** - * @var boolean whether to enable ANSI color in the output. - * If not set, ANSI color will only be enabled for terminals that support it. - */ - public $color; + /** + * @var boolean whether to enable ANSI color in the output. + * If not set, ANSI color will only be enabled for terminals that support it. + */ + public $color; - /** - * Returns a value indicating whether ANSI color is enabled. - * - * ANSI color is enabled only if [[color]] is set true or is not set - * and the terminal supports ANSI color. - * - * @param resource $stream the stream to check. - * @return boolean Whether to enable ANSI style in output. - */ - public function isColorEnabled($stream = STDOUT) - { - return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color; - } + /** + * Returns a value indicating whether ANSI color is enabled. + * + * ANSI color is enabled only if [[color]] is set true or is not set + * and the terminal supports ANSI color. + * + * @param resource $stream the stream to check. + * @return boolean Whether to enable ANSI style in output. + */ + public function isColorEnabled($stream = STDOUT) + { + return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color; + } - /** - * Runs an action with the specified action ID and parameters. - * If the action ID is empty, the method will use [[defaultAction]]. - * @param string $id the ID of the action to be executed. - * @param array $params the parameters (name-value pairs) to be passed to the action. - * @return integer the status of the action execution. 0 means normal, other values mean abnormal. - * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. - * @throws Exception if there are unknown options or missing arguments - * @see createAction - */ - public function runAction($id, $params = []) - { - if (!empty($params)) { - // populate options here so that they are available in beforeAction(). - $options = $this->options($id); - foreach ($params as $name => $value) { - if (in_array($name, $options, true)) { - $default = $this->$name; - $this->$name = is_array($default) ? preg_split('/\s*,\s*/', $value) : $value; - unset($params[$name]); - } elseif (!is_int($name)) { - throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name])); - } - } - } - return parent::runAction($id, $params); - } + /** + * Runs an action with the specified action ID and parameters. + * If the action ID is empty, the method will use [[defaultAction]]. + * @param string $id the ID of the action to be executed. + * @param array $params the parameters (name-value pairs) to be passed to the action. + * @return integer the status of the action execution. 0 means normal, other values mean abnormal. + * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. + * @throws Exception if there are unknown options or missing arguments + * @see createAction + */ + public function runAction($id, $params = []) + { + if (!empty($params)) { + // populate options here so that they are available in beforeAction(). + $options = $this->options($id); + foreach ($params as $name => $value) { + if (in_array($name, $options, true)) { + $default = $this->$name; + $this->$name = is_array($default) ? preg_split('/\s*,\s*/', $value) : $value; + unset($params[$name]); + } elseif (!is_int($name)) { + throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name])); + } + } + } - /** - * Binds the parameters to the action. - * This method is invoked by [[Action]] when it begins to run with the given parameters. - * This method will first bind the parameters with the [[options()|options]] - * available to the action. It then validates the given arguments. - * @param Action $action the action to be bound with parameters - * @param array $params the parameters to be bound to the action - * @return array the valid parameters that the action can run with. - * @throws Exception if there are unknown options or missing arguments - */ - public function bindActionParams($action, $params) - { - if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($this, $action->actionMethod); - } else { - $method = new \ReflectionMethod($action, 'run'); - } + return parent::runAction($id, $params); + } - $args = array_values($params); + /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will first bind the parameters with the [[options()|options]] + * available to the action. It then validates the given arguments. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws Exception if there are unknown options or missing arguments + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } - $missing = []; - foreach ($method->getParameters() as $i => $param) { - if ($param->isArray() && isset($args[$i])) { - $args[$i] = preg_split('/\s*,\s*/', $args[$i]); - } - if (!isset($args[$i])) { - if ($param->isDefaultValueAvailable()) { - $args[$i] = $param->getDefaultValue(); - } else { - $missing[] = $param->getName(); - } - } - } + $args = array_values($params); - if (!empty($missing)) { - throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)])); - } + $missing = []; + foreach ($method->getParameters() as $i => $param) { + if ($param->isArray() && isset($args[$i])) { + $args[$i] = preg_split('/\s*,\s*/', $args[$i]); + } + if (!isset($args[$i])) { + if ($param->isDefaultValueAvailable()) { + $args[$i] = $param->getDefaultValue(); + } else { + $missing[] = $param->getName(); + } + } + } - return $args; - } + if (!empty($missing)) { + throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)])); + } - /** - * Formats a string with ANSI codes - * - * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]]. - * - * Example: - * - * ~~~ - * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); - * ~~~ - * - * @param string $string the string to be formatted - * @return string - */ - public function ansiFormat($string) - { - if ($this->isColorEnabled()) { - $args = func_get_args(); - array_shift($args); - $string = Console::ansiFormat($string, $args); - } - return $string; - } + return $args; + } - /** - * Prints a string to STDOUT - * - * You may optionally format the string with ANSI codes by - * passing additional parameters using the constants defined in [[\yii\helpers\Console]]. - * - * Example: - * - * ~~~ - * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); - * ~~~ - * - * @param string $string the string to print - * @return int|boolean Number of bytes printed or false on error - */ - public function stdout($string) - { - if ($this->isColorEnabled()) { - $args = func_get_args(); - array_shift($args); - $string = Console::ansiFormat($string, $args); - } - return Console::stdout($string); - } + /** + * Formats a string with ANSI codes + * + * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]]. + * + * Example: + * + * ~~~ + * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to be formatted + * @return string + */ + public function ansiFormat($string) + { + if ($this->isColorEnabled()) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } - /** - * Prints a string to STDERR - * - * You may optionally format the string with ANSI codes by - * passing additional parameters using the constants defined in [[\yii\helpers\Console]]. - * - * Example: - * - * ~~~ - * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); - * ~~~ - * - * @param string $string the string to print - * @return int|boolean Number of bytes printed or false on error - */ - public function stderr($string) - { - if ($this->isColorEnabled(STDERR)) { - $args = func_get_args(); - array_shift($args); - $string = Console::ansiFormat($string, $args); - } - return fwrite(STDERR, $string); - } + return $string; + } - /** - * Prompts the user for input and validates it - * - * @param string $text prompt string - * @param array $options the options to validate the input: - * - * - required: whether it is required or not - * - default: default value if no input is inserted by the user - * - pattern: regular expression pattern to validate user input - * - validator: a callable function to validate input. The function must accept two parameters: - * - $input: the user input to validate - * - $error: the error value passed by reference if validation failed. - * @return string the user input - */ - public function prompt($text, $options = []) - { - if ($this->interactive) { - return Console::prompt($text, $options); - } else { - return isset($options['default']) ? $options['default'] : ''; - } - } + /** + * Prints a string to STDOUT + * + * You may optionally format the string with ANSI codes by + * passing additional parameters using the constants defined in [[\yii\helpers\Console]]. + * + * Example: + * + * ~~~ + * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to print + * @return int|boolean Number of bytes printed or false on error + */ + public function stdout($string) + { + if ($this->isColorEnabled()) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } - /** - * Asks user to confirm by typing y or n. - * - * @param string $message to echo out before waiting for user input - * @param boolean $default this value is returned if no selection is made. - * @return boolean whether user confirmed. - * Will return true if [[interactive]] is false. - */ - public function confirm($message, $default = false) - { - if ($this->interactive) { - return Console::confirm($message, $default); - } else { - return true; - } - } + return Console::stdout($string); + } - /** - * Gives the user an option to choose from. Giving '?' as an input will show - * a list of options to choose from and their explanations. - * - * @param string $prompt the prompt message - * @param array $options Key-value array of options to choose from - * - * @return string An option character the user chose - */ - public function select($prompt, $options = []) - { - return Console::select($prompt, $options); - } + /** + * Prints a string to STDERR + * + * You may optionally format the string with ANSI codes by + * passing additional parameters using the constants defined in [[\yii\helpers\Console]]. + * + * Example: + * + * ~~~ + * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to print + * @return int|boolean Number of bytes printed or false on error + */ + public function stderr($string) + { + if ($this->isColorEnabled(STDERR)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return fwrite(STDERR, $string); + } - /** - * Returns the names of valid options for the action (id) - * An option requires the existence of a public member variable whose - * name is the option name. - * Child classes may override this method to specify possible options. - * - * Note that the values setting via options are not available - * until [[beforeAction()]] is being called. - * - * @param $id action name - * @return array the names of the options valid for the action - */ - public function options($id) - { - // $id might be used in subclass to provide options specific to action id - return ['color', 'interactive']; - } + /** + * Prompts the user for input and validates it + * + * @param string $text prompt string + * @param array $options the options to validate the input: + * + * - required: whether it is required or not + * - default: default value if no input is inserted by the user + * - pattern: regular expression pattern to validate user input + * - validator: a callable function to validate input. The function must accept two parameters: + * - $input: the user input to validate + * - $error: the error value passed by reference if validation failed. + * @return string the user input + */ + public function prompt($text, $options = []) + { + if ($this->interactive) { + return Console::prompt($text, $options); + } else { + return isset($options['default']) ? $options['default'] : ''; + } + } + + /** + * Asks user to confirm by typing y or n. + * + * @param string $message to echo out before waiting for user input + * @param boolean $default this value is returned if no selection is made. + * @return boolean whether user confirmed. + * Will return true if [[interactive]] is false. + */ + public function confirm($message, $default = false) + { + if ($this->interactive) { + return Console::confirm($message, $default); + } else { + return true; + } + } + + /** + * Gives the user an option to choose from. Giving '?' as an input will show + * a list of options to choose from and their explanations. + * + * @param string $prompt the prompt message + * @param array $options Key-value array of options to choose from + * + * @return string An option character the user chose + */ + public function select($prompt, $options = []) + { + return Console::select($prompt, $options); + } + + /** + * Returns the names of valid options for the action (id) + * An option requires the existence of a public member variable whose + * name is the option name. + * Child classes may override this method to specify possible options. + * + * Note that the values setting via options are not available + * until [[beforeAction()]] is being called. + * + * @param $id action name + * @return array the names of the options valid for the action + */ + public function options($id) + { + // $id might be used in subclass to provide options specific to action id + return ['color', 'interactive']; + } } diff --git a/framework/console/Exception.php b/framework/console/Exception.php index 5148361bffa..41a01e475a0 100644 --- a/framework/console/Exception.php +++ b/framework/console/Exception.php @@ -17,11 +17,11 @@ */ class Exception extends UserException { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Error'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Error'; + } } diff --git a/framework/console/Request.php b/framework/console/Request.php index e5aa9fdac3f..fa017e4816d 100644 --- a/framework/console/Request.php +++ b/framework/console/Request.php @@ -20,60 +20,61 @@ */ class Request extends \yii\base\Request { - private $_params; + private $_params; - /** - * Returns the command line arguments. - * @return array the command line arguments. It does not include the entry script name. - */ - public function getParams() - { - if (!isset($this->_params)) { - if (isset($_SERVER['argv'])) { - $this->_params = $_SERVER['argv']; - array_shift($this->_params); - } else { - $this->_params = []; - } - } - return $this->_params; - } + /** + * Returns the command line arguments. + * @return array the command line arguments. It does not include the entry script name. + */ + public function getParams() + { + if (!isset($this->_params)) { + if (isset($_SERVER['argv'])) { + $this->_params = $_SERVER['argv']; + array_shift($this->_params); + } else { + $this->_params = []; + } + } - /** - * Sets the command line arguments. - * @param array $params the command line arguments - */ - public function setParams($params) - { - $this->_params = $params; - } + return $this->_params; + } - /** - * Resolves the current request into a route and the associated parameters. - * @return array the first element is the route, and the second is the associated parameters. - */ - public function resolve() - { - $rawParams = $this->getParams(); - if (isset($rawParams[0])) { - $route = $rawParams[0]; - array_shift($rawParams); - } else { - $route = ''; - } + /** + * Sets the command line arguments. + * @param array $params the command line arguments + */ + public function setParams($params) + { + $this->_params = $params; + } - $params = []; - foreach ($rawParams as $param) { - if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { - $name = $matches[1]; - if ($name !== Application::OPTION_APPCONFIG) { - $params[$name] = isset($matches[3]) ? $matches[3] : true; - } - } else { - $params[] = $param; - } - } + /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + */ + public function resolve() + { + $rawParams = $this->getParams(); + if (isset($rawParams[0])) { + $route = $rawParams[0]; + array_shift($rawParams); + } else { + $route = ''; + } - return [$route, $params]; - } + $params = []; + foreach ($rawParams as $param) { + if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { + $name = $matches[1]; + if ($name !== Application::OPTION_APPCONFIG) { + $params[$name] = isset($matches[3]) ? $matches[3] : true; + } + } else { + $params[] = $param; + } + } + + return [$route, $params]; + } } diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php index 5f5eeaa2eb3..6f71365d1d0 100644 --- a/framework/console/controllers/AssetController.php +++ b/framework/console/controllers/AssetController.php @@ -36,363 +36,365 @@ */ class AssetController extends Controller { - /** - * @var string controller default action ID. - */ - public $defaultAction = 'compress'; - /** - * @var array list of asset bundles to be compressed. - */ - public $bundles = []; - /** - * @var array list of asset bundles, which represents output compressed files. - * You can specify the name of the output compressed file using 'css' and 'js' keys: - * For example: - * - * ~~~ - * 'app\config\AllAsset' => [ - * 'js' => 'js/all-{ts}.js', - * 'css' => 'css/all-{ts}.css', - * 'depends' => [ ... ], - * ] - * ~~~ - * - * File names can contain placeholder "{ts}", which will be filled by current timestamp, while - * file creation. - */ - public $targets = []; - /** - * @var string|callable JavaScript file compressor. - * If a string, it is treated as shell command template, which should contain - * placeholders {from} - source file name - and {to} - output file name. - * Otherwise, it is treated as PHP callback, which should perform the compression. - * - * Default value relies on usage of "Closure Compiler" - * @see https://developers.google.com/closure/compiler/ - */ - public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; - /** - * @var string|callable CSS file compressor. - * If a string, it is treated as shell command template, which should contain - * placeholders {from} - source file name - and {to} - output file name. - * Otherwise, it is treated as PHP callback, which should perform the compression. - * - * Default value relies on usage of "YUI Compressor" - * @see https://github.com/yui/yuicompressor/ - */ - public $cssCompressor = 'java -jar yuicompressor.jar --type css {from} -o {to}'; - - /** - * @var array|\yii\web\AssetManager [[\yii\web\AssetManager]] instance or its array configuration, which will be used - * for assets processing. - */ - private $_assetManager = []; - - - /** - * Returns the asset manager instance. - * @throws \yii\console\Exception on invalid configuration. - * @return \yii\web\AssetManager asset manager instance. - */ - public function getAssetManager() - { - if (!is_object($this->_assetManager)) { - $options = $this->_assetManager; - if (!isset($options['class'])) { - $options['class'] = 'yii\\web\\AssetManager'; - } - if (!isset($options['basePath'])) { - throw new Exception("Please specify 'basePath' for the 'assetManager' option."); - } - if (!isset($options['baseUrl'])) { - throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); - } - $this->_assetManager = Yii::createObject($options); - } - return $this->_assetManager; - } - - /** - * Sets asset manager instance or configuration. - * @param \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration. - * @throws \yii\console\Exception on invalid argument type. - */ - public function setAssetManager($assetManager) - { - if (is_scalar($assetManager)) { - throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.'); - } - $this->_assetManager = $assetManager; - } - - /** - * Combines and compresses the asset files according to the given configuration. - * During the process new asset bundle configuration file will be created. - * You should replace your original asset bundle configuration with this file in order to use compressed files. - * @param string $configFile configuration file name. - * @param string $bundleFile output asset bundles configuration file name. - */ - public function actionCompress($configFile, $bundleFile) - { - $this->loadConfiguration($configFile); - $bundles = $this->loadBundles($this->bundles); - $targets = $this->loadTargets($this->targets, $bundles); - $timestamp = time(); - foreach ($targets as $name => $target) { - echo "Creating output bundle '{$name}':\n"; - if (!empty($target->js)) { - $this->buildTarget($target, 'js', $bundles, $timestamp); - } - if (!empty($target->css)) { - $this->buildTarget($target, 'css', $bundles, $timestamp); - } - echo "\n"; - } - - $targets = $this->adjustDependency($targets, $bundles); - $this->saveTargets($targets, $bundleFile); - } - - /** - * Applies configuration from the given file to self instance. - * @param string $configFile configuration file name. - * @throws \yii\console\Exception on failure. - */ - protected function loadConfiguration($configFile) - { - echo "Loading configuration from '{$configFile}'...\n"; - foreach (require($configFile) as $name => $value) { - if (property_exists($this, $name) || $this->canSetProperty($name)) { - $this->$name = $value; - } else { - throw new Exception("Unknown configuration option: $name"); - } - } - - $this->getAssetManager(); // check if asset manager configuration is correct - } - - /** - * Creates full list of source asset bundles. - * @param string[] $bundles list of asset bundle names - * @return \yii\web\AssetBundle[] list of source asset bundles. - */ - protected function loadBundles($bundles) - { - echo "Collecting source bundles information...\n"; - - $am = $this->getAssetManager(); - $result = []; - foreach ($bundles as $name) { - $result[$name] = $am->getBundle($name); - } - foreach ($result as $bundle) { - $this->loadDependency($bundle, $result); - } - - return $result; - } - - /** - * Loads asset bundle dependencies recursively. - * @param \yii\web\AssetBundle $bundle bundle instance - * @param array $result already loaded bundles list. - * @throws Exception on failure. - */ - protected function loadDependency($bundle, &$result) - { - $am = $this->getAssetManager(); - foreach ($bundle->depends as $name) { - if (!isset($result[$name])) { - $dependencyBundle = $am->getBundle($name); - $result[$name] = false; - $this->loadDependency($dependencyBundle, $result); - $result[$name] = $dependencyBundle; - } elseif ($result[$name] === false) { - throw new Exception("A circular dependency is detected for bundle '$name'."); - } - } - } - - /** - * Creates full list of output asset bundles. - * @param array $targets output asset bundles configuration. - * @param \yii\web\AssetBundle[] $bundles list of source asset bundles. - * @return \yii\web\AssetBundle[] list of output asset bundles. - * @throws Exception on failure. - */ - protected function loadTargets($targets, $bundles) - { - // build the dependency order of bundles - $registered = []; - foreach ($bundles as $name => $bundle) { - $this->registerBundle($bundles, $name, $registered); - } - $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); - - // fill up the target which has empty 'depends'. - $referenced = []; - foreach ($targets as $name => $target) { - if (empty($target['depends'])) { - if (!isset($all)) { - $all = $name; - } else { - throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); - } - } else { - foreach ($target['depends'] as $bundle) { - if (!isset($referenced[$bundle])) { - $referenced[$bundle] = $name; - } else { - throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); - } - } - } - } - if (isset($all)) { - $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); - } - - // adjust the 'depends' order for each target according to the dependency order of bundles - // create an AssetBundle object for each target - foreach ($targets as $name => $target) { - if (!isset($target['basePath'])) { - throw new Exception("Please specify 'basePath' for the '$name' target."); - } - if (!isset($target['baseUrl'])) { - throw new Exception("Please specify 'baseUrl' for the '$name' target."); - } - usort($target['depends'], function ($a, $b) use ($bundleOrders) { - if ($bundleOrders[$a] == $bundleOrders[$b]) { - return 0; - } else { - return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; - } - }); - $target['class'] = $name; - $targets[$name] = Yii::createObject($target); - } - return $targets; - } - - /** - * Builds output asset bundle. - * @param \yii\web\AssetBundle $target output asset bundle - * @param string $type either 'js' or 'css'. - * @param \yii\web\AssetBundle[] $bundles source asset bundles. - * @param integer $timestamp current timestamp. - * @throws Exception on failure. - */ - protected function buildTarget($target, $type, $bundles, $timestamp) - { - $outputFile = strtr($target->$type, [ - '{ts}' => $timestamp, - ]); - $inputFiles = []; - - foreach ($target->depends as $name) { - if (isset($bundles[$name])) { - foreach ($bundles[$name]->$type as $file) { - $inputFiles[] = $bundles[$name]->basePath . '/' . $file; - } - } else { - throw new Exception("Unknown bundle: '{$name}'"); - } - } - if ($type === 'js') { - $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); - } else { - $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); - } - $target->$type = [$outputFile]; - } - - /** - * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones. - * @param \yii\web\AssetBundle[] $targets output asset bundles. - * @param \yii\web\AssetBundle[] $bundles source asset bundles. - * @return \yii\web\AssetBundle[] output asset bundles. - */ - protected function adjustDependency($targets, $bundles) - { - echo "Creating new bundle configuration...\n"; - - $map = []; - foreach ($targets as $name => $target) { - foreach ($target->depends as $bundle) { - $map[$bundle] = $name; - } - } - - foreach ($targets as $name => $target) { - $depends = []; - foreach ($target->depends as $bn) { - foreach ($bundles[$bn]->depends as $bundle) { - $depends[$map[$bundle]] = true; - } - } - unset($depends[$name]); - $target->depends = array_keys($depends); - } - - // detect possible circular dependencies - foreach ($targets as $name => $target) { - $registered = []; - $this->registerBundle($targets, $name, $registered); - } - - foreach ($map as $bundle => $target) { - $targets[$bundle] = Yii::createObject([ - 'class' => 'yii\\web\\AssetBundle', - 'depends' => [$target], - ]); - } - return $targets; - } - - /** - * Registers asset bundles including their dependencies. - * @param \yii\web\AssetBundle[] $bundles asset bundles list. - * @param string $name bundle name. - * @param array $registered stores already registered names. - * @throws Exception if circular dependency is detected. - */ - protected function registerBundle($bundles, $name, &$registered) - { - if (!isset($registered[$name])) { - $registered[$name] = false; - $bundle = $bundles[$name]; - foreach ($bundle->depends as $depend) { - $this->registerBundle($bundles, $depend, $registered); - } - unset($registered[$name]); - $registered[$name] = true; - } elseif ($registered[$name] === false) { - throw new Exception("A circular dependency is detected for target '$name'."); - } - } - - /** - * Saves new asset bundles configuration. - * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved. - * @param string $bundleFile output file name. - * @throws \yii\console\Exception on failure. - */ - protected function saveTargets($targets, $bundleFile) - { - $array = []; - foreach ($targets as $name => $target) { - foreach (['basePath', 'baseUrl', 'js', 'css', 'depends'] as $prop) { - if (!empty($target->$prop)) { - $array[$name][$prop] = $target->$prop; - } elseif (in_array($prop, ['js', 'css'])) { - $array[$name][$prop] = []; - } - } - } - $array = var_export($array, true); - $version = date('Y-m-d H:i:s', time()); - $bundleFileContent = << [ + * 'js' => 'js/all-{ts}.js', + * 'css' => 'css/all-{ts}.css', + * 'depends' => [ ... ], + * ] + * ~~~ + * + * File names can contain placeholder "{ts}", which will be filled by current timestamp, while + * file creation. + */ + public $targets = []; + /** + * @var string|callable JavaScript file compressor. + * If a string, it is treated as shell command template, which should contain + * placeholders {from} - source file name - and {to} - output file name. + * Otherwise, it is treated as PHP callback, which should perform the compression. + * + * Default value relies on usage of "Closure Compiler" + * @see https://developers.google.com/closure/compiler/ + */ + public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; + /** + * @var string|callable CSS file compressor. + * If a string, it is treated as shell command template, which should contain + * placeholders {from} - source file name - and {to} - output file name. + * Otherwise, it is treated as PHP callback, which should perform the compression. + * + * Default value relies on usage of "YUI Compressor" + * @see https://github.com/yui/yuicompressor/ + */ + public $cssCompressor = 'java -jar yuicompressor.jar --type css {from} -o {to}'; + + /** + * @var array|\yii\web\AssetManager [[\yii\web\AssetManager]] instance or its array configuration, which will be used + * for assets processing. + */ + private $_assetManager = []; + + /** + * Returns the asset manager instance. + * @throws \yii\console\Exception on invalid configuration. + * @return \yii\web\AssetManager asset manager instance. + */ + public function getAssetManager() + { + if (!is_object($this->_assetManager)) { + $options = $this->_assetManager; + if (!isset($options['class'])) { + $options['class'] = 'yii\\web\\AssetManager'; + } + if (!isset($options['basePath'])) { + throw new Exception("Please specify 'basePath' for the 'assetManager' option."); + } + if (!isset($options['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); + } + $this->_assetManager = Yii::createObject($options); + } + + return $this->_assetManager; + } + + /** + * Sets asset manager instance or configuration. + * @param \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration. + * @throws \yii\console\Exception on invalid argument type. + */ + public function setAssetManager($assetManager) + { + if (is_scalar($assetManager)) { + throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.'); + } + $this->_assetManager = $assetManager; + } + + /** + * Combines and compresses the asset files according to the given configuration. + * During the process new asset bundle configuration file will be created. + * You should replace your original asset bundle configuration with this file in order to use compressed files. + * @param string $configFile configuration file name. + * @param string $bundleFile output asset bundles configuration file name. + */ + public function actionCompress($configFile, $bundleFile) + { + $this->loadConfiguration($configFile); + $bundles = $this->loadBundles($this->bundles); + $targets = $this->loadTargets($this->targets, $bundles); + $timestamp = time(); + foreach ($targets as $name => $target) { + echo "Creating output bundle '{$name}':\n"; + if (!empty($target->js)) { + $this->buildTarget($target, 'js', $bundles, $timestamp); + } + if (!empty($target->css)) { + $this->buildTarget($target, 'css', $bundles, $timestamp); + } + echo "\n"; + } + + $targets = $this->adjustDependency($targets, $bundles); + $this->saveTargets($targets, $bundleFile); + } + + /** + * Applies configuration from the given file to self instance. + * @param string $configFile configuration file name. + * @throws \yii\console\Exception on failure. + */ + protected function loadConfiguration($configFile) + { + echo "Loading configuration from '{$configFile}'...\n"; + foreach (require($configFile) as $name => $value) { + if (property_exists($this, $name) || $this->canSetProperty($name)) { + $this->$name = $value; + } else { + throw new Exception("Unknown configuration option: $name"); + } + } + + $this->getAssetManager(); // check if asset manager configuration is correct + } + + /** + * Creates full list of source asset bundles. + * @param string[] $bundles list of asset bundle names + * @return \yii\web\AssetBundle[] list of source asset bundles. + */ + protected function loadBundles($bundles) + { + echo "Collecting source bundles information...\n"; + + $am = $this->getAssetManager(); + $result = []; + foreach ($bundles as $name) { + $result[$name] = $am->getBundle($name); + } + foreach ($result as $bundle) { + $this->loadDependency($bundle, $result); + } + + return $result; + } + + /** + * Loads asset bundle dependencies recursively. + * @param \yii\web\AssetBundle $bundle bundle instance + * @param array $result already loaded bundles list. + * @throws Exception on failure. + */ + protected function loadDependency($bundle, &$result) + { + $am = $this->getAssetManager(); + foreach ($bundle->depends as $name) { + if (!isset($result[$name])) { + $dependencyBundle = $am->getBundle($name); + $result[$name] = false; + $this->loadDependency($dependencyBundle, $result); + $result[$name] = $dependencyBundle; + } elseif ($result[$name] === false) { + throw new Exception("A circular dependency is detected for bundle '$name'."); + } + } + } + + /** + * Creates full list of output asset bundles. + * @param array $targets output asset bundles configuration. + * @param \yii\web\AssetBundle[] $bundles list of source asset bundles. + * @return \yii\web\AssetBundle[] list of output asset bundles. + * @throws Exception on failure. + */ + protected function loadTargets($targets, $bundles) + { + // build the dependency order of bundles + $registered = []; + foreach ($bundles as $name => $bundle) { + $this->registerBundle($bundles, $name, $registered); + } + $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); + + // fill up the target which has empty 'depends'. + $referenced = []; + foreach ($targets as $name => $target) { + if (empty($target['depends'])) { + if (!isset($all)) { + $all = $name; + } else { + throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); + } + } else { + foreach ($target['depends'] as $bundle) { + if (!isset($referenced[$bundle])) { + $referenced[$bundle] = $name; + } else { + throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); + } + } + } + } + if (isset($all)) { + $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); + } + + // adjust the 'depends' order for each target according to the dependency order of bundles + // create an AssetBundle object for each target + foreach ($targets as $name => $target) { + if (!isset($target['basePath'])) { + throw new Exception("Please specify 'basePath' for the '$name' target."); + } + if (!isset($target['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the '$name' target."); + } + usort($target['depends'], function ($a, $b) use ($bundleOrders) { + if ($bundleOrders[$a] == $bundleOrders[$b]) { + return 0; + } else { + return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; + } + }); + $target['class'] = $name; + $targets[$name] = Yii::createObject($target); + } + + return $targets; + } + + /** + * Builds output asset bundle. + * @param \yii\web\AssetBundle $target output asset bundle + * @param string $type either 'js' or 'css'. + * @param \yii\web\AssetBundle[] $bundles source asset bundles. + * @param integer $timestamp current timestamp. + * @throws Exception on failure. + */ + protected function buildTarget($target, $type, $bundles, $timestamp) + { + $outputFile = strtr($target->$type, [ + '{ts}' => $timestamp, + ]); + $inputFiles = []; + + foreach ($target->depends as $name) { + if (isset($bundles[$name])) { + foreach ($bundles[$name]->$type as $file) { + $inputFiles[] = $bundles[$name]->basePath . '/' . $file; + } + } else { + throw new Exception("Unknown bundle: '{$name}'"); + } + } + if ($type === 'js') { + $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); + } else { + $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); + } + $target->$type = [$outputFile]; + } + + /** + * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones. + * @param \yii\web\AssetBundle[] $targets output asset bundles. + * @param \yii\web\AssetBundle[] $bundles source asset bundles. + * @return \yii\web\AssetBundle[] output asset bundles. + */ + protected function adjustDependency($targets, $bundles) + { + echo "Creating new bundle configuration...\n"; + + $map = []; + foreach ($targets as $name => $target) { + foreach ($target->depends as $bundle) { + $map[$bundle] = $name; + } + } + + foreach ($targets as $name => $target) { + $depends = []; + foreach ($target->depends as $bn) { + foreach ($bundles[$bn]->depends as $bundle) { + $depends[$map[$bundle]] = true; + } + } + unset($depends[$name]); + $target->depends = array_keys($depends); + } + + // detect possible circular dependencies + foreach ($targets as $name => $target) { + $registered = []; + $this->registerBundle($targets, $name, $registered); + } + + foreach ($map as $bundle => $target) { + $targets[$bundle] = Yii::createObject([ + 'class' => 'yii\\web\\AssetBundle', + 'depends' => [$target], + ]); + } + + return $targets; + } + + /** + * Registers asset bundles including their dependencies. + * @param \yii\web\AssetBundle[] $bundles asset bundles list. + * @param string $name bundle name. + * @param array $registered stores already registered names. + * @throws Exception if circular dependency is detected. + */ + protected function registerBundle($bundles, $name, &$registered) + { + if (!isset($registered[$name])) { + $registered[$name] = false; + $bundle = $bundles[$name]; + foreach ($bundle->depends as $depend) { + $this->registerBundle($bundles, $depend, $registered); + } + unset($registered[$name]); + $registered[$name] = true; + } elseif ($registered[$name] === false) { + throw new Exception("A circular dependency is detected for target '$name'."); + } + } + + /** + * Saves new asset bundles configuration. + * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved. + * @param string $bundleFile output file name. + * @throws \yii\console\Exception on failure. + */ + protected function saveTargets($targets, $bundleFile) + { + $array = []; + foreach ($targets as $name => $target) { + foreach (['basePath', 'baseUrl', 'js', 'css', 'depends'] as $prop) { + if (!empty($target->$prop)) { + $array[$name][$prop] = $target->$prop; + } elseif (in_array($prop, ['js', 'css'])) { + $array[$name][$prop] = []; + } + } + } + $array = var_export($array, true); + $version = date('Y-m-d H:i:s', time()); + $bundleFileContent = <<id}" command. @@ -401,177 +403,177 @@ protected function saveTargets($targets, $bundleFile) */ return {$array}; EOD; - if (!file_put_contents($bundleFile, $bundleFileContent)) { - throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'."); - } - echo "Output bundle configuration created at '{$bundleFile}'.\n"; - } - - /** - * Compresses given JavaScript files and combines them into the single one. - * @param array $inputFiles list of source file names. - * @param string $outputFile output file name. - * @throws \yii\console\Exception on failure - */ - protected function compressJsFiles($inputFiles, $outputFile) - { - if (empty($inputFiles)) { - return; - } - echo " Compressing JavaScript files...\n"; - if (is_string($this->jsCompressor)) { - $tmpFile = $outputFile . '.tmp'; - $this->combineJsFiles($inputFiles, $tmpFile); - echo shell_exec(strtr($this->jsCompressor, [ - '{from}' => escapeshellarg($tmpFile), - '{to}' => escapeshellarg($outputFile), - ])); - @unlink($tmpFile); - } else { - call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); - } - if (!file_exists($outputFile)) { - throw new Exception("Unable to compress JavaScript files into '{$outputFile}'."); - } - echo " JavaScript files compressed into '{$outputFile}'.\n"; - } - - /** - * Compresses given CSS files and combines them into the single one. - * @param array $inputFiles list of source file names. - * @param string $outputFile output file name. - * @throws \yii\console\Exception on failure - */ - protected function compressCssFiles($inputFiles, $outputFile) - { - if (empty($inputFiles)) { - return; - } - echo " Compressing CSS files...\n"; - if (is_string($this->cssCompressor)) { - $tmpFile = $outputFile . '.tmp'; - $this->combineCssFiles($inputFiles, $tmpFile); - echo shell_exec(strtr($this->cssCompressor, [ - '{from}' => escapeshellarg($tmpFile), - '{to}' => escapeshellarg($outputFile), - ])); - @unlink($tmpFile); - } else { - call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); - } - if (!file_exists($outputFile)) { - throw new Exception("Unable to compress CSS files into '{$outputFile}'."); - } - echo " CSS files compressed into '{$outputFile}'.\n"; - } - - /** - * Combines JavaScript files into a single one. - * @param array $inputFiles source file names. - * @param string $outputFile output file name. - * @throws \yii\console\Exception on failure. - */ - public function combineJsFiles($inputFiles, $outputFile) - { - $content = ''; - foreach ($inputFiles as $file) { - $content .= "/*** BEGIN FILE: $file ***/\n" - . file_get_contents($file) - . "/*** END FILE: $file ***/\n"; - } - if (!file_put_contents($outputFile, $content)) { - throw new Exception("Unable to write output JavaScript file '{$outputFile}'."); - } - } - - /** - * Combines CSS files into a single one. - * @param array $inputFiles source file names. - * @param string $outputFile output file name. - * @throws \yii\console\Exception on failure. - */ - public function combineCssFiles($inputFiles, $outputFile) - { - $content = ''; - foreach ($inputFiles as $file) { - $content .= "/*** BEGIN FILE: $file ***/\n" - . $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile)) - . "/*** END FILE: $file ***/\n"; - } - if (!file_put_contents($outputFile, $content)) { - throw new Exception("Unable to write output CSS file '{$outputFile}'."); - } - } - - /** - * Adjusts CSS content allowing URL references pointing to the original resources. - * @param string $cssContent source CSS content. - * @param string $inputFilePath input CSS file name. - * @param string $outputFilePath output CSS file name. - * @return string adjusted CSS content. - */ - protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath) - { - $sharedPathParts = []; - $inputFilePathParts = explode('/', $inputFilePath); - $inputFilePathPartsCount = count($inputFilePathParts); - $outputFilePathParts = explode('/', $outputFilePath); - $outputFilePathPartsCount = count($outputFilePathParts); - for ($i =0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) { - if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) { - $sharedPathParts[] = $inputFilePathParts[$i]; - } else { - break; - } - } - $sharedPath = implode('/', $sharedPathParts); - - $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/'); - $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/'); - $inputFileRelativePathParts = explode('/', $inputFileRelativePath); - $outputFileRelativePathParts = explode('/', $outputFileRelativePath); - - $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { - $fullMatch = $matches[0]; - $inputUrl = $matches[1]; - - if (preg_match('/https?:\/\//is', $inputUrl)) { - return $fullMatch; - } - - $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..'); - $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts); - - if (strpos($inputUrl, '/') !== false) { - $inputUrlParts = explode('/', $inputUrl); - foreach ($inputUrlParts as $key => $inputUrlPart) { - if ($inputUrlPart == '..') { - array_pop($outputUrlParts); - unset($inputUrlParts[$key]); - } - } - $outputUrlParts[] = implode('/', $inputUrlParts); - } else { - $outputUrlParts[] = $inputUrl; - } - $outputUrl = implode('/', $outputUrlParts); - - return str_replace($inputUrl, $outputUrl, $fullMatch); - }; - - $cssContent = preg_replace_callback('/url\(["\']?([^)^"^\']*)["\']?\)/is', $callback, $cssContent); - - return $cssContent; - } - - /** - * Creates template of configuration file for [[actionCompress]]. - * @param string $configFile output file name. - * @throws \yii\console\Exception on failure. - */ - public function actionTemplate($configFile) - { - $template = <<jsCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineJsFiles($inputFiles, $tmpFile); + echo shell_exec(strtr($this->jsCompressor, [ + '{from}' => escapeshellarg($tmpFile), + '{to}' => escapeshellarg($outputFile), + ])); + @unlink($tmpFile); + } else { + call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); + } + if (!file_exists($outputFile)) { + throw new Exception("Unable to compress JavaScript files into '{$outputFile}'."); + } + echo " JavaScript files compressed into '{$outputFile}'.\n"; + } + + /** + * Compresses given CSS files and combines them into the single one. + * @param array $inputFiles list of source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure + */ + protected function compressCssFiles($inputFiles, $outputFile) + { + if (empty($inputFiles)) { + return; + } + echo " Compressing CSS files...\n"; + if (is_string($this->cssCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineCssFiles($inputFiles, $tmpFile); + echo shell_exec(strtr($this->cssCompressor, [ + '{from}' => escapeshellarg($tmpFile), + '{to}' => escapeshellarg($outputFile), + ])); + @unlink($tmpFile); + } else { + call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); + } + if (!file_exists($outputFile)) { + throw new Exception("Unable to compress CSS files into '{$outputFile}'."); + } + echo " CSS files compressed into '{$outputFile}'.\n"; + } + + /** + * Combines JavaScript files into a single one. + * @param array $inputFiles source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure. + */ + public function combineJsFiles($inputFiles, $outputFile) + { + $content = ''; + foreach ($inputFiles as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + if (!file_put_contents($outputFile, $content)) { + throw new Exception("Unable to write output JavaScript file '{$outputFile}'."); + } + } + + /** + * Combines CSS files into a single one. + * @param array $inputFiles source file names. + * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure. + */ + public function combineCssFiles($inputFiles, $outputFile) + { + $content = ''; + foreach ($inputFiles as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile)) + . "/*** END FILE: $file ***/\n"; + } + if (!file_put_contents($outputFile, $content)) { + throw new Exception("Unable to write output CSS file '{$outputFile}'."); + } + } + + /** + * Adjusts CSS content allowing URL references pointing to the original resources. + * @param string $cssContent source CSS content. + * @param string $inputFilePath input CSS file name. + * @param string $outputFilePath output CSS file name. + * @return string adjusted CSS content. + */ + protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath) + { + $sharedPathParts = []; + $inputFilePathParts = explode('/', $inputFilePath); + $inputFilePathPartsCount = count($inputFilePathParts); + $outputFilePathParts = explode('/', $outputFilePath); + $outputFilePathPartsCount = count($outputFilePathParts); + for ($i =0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) { + if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) { + $sharedPathParts[] = $inputFilePathParts[$i]; + } else { + break; + } + } + $sharedPath = implode('/', $sharedPathParts); + + $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/'); + $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/'); + $inputFileRelativePathParts = explode('/', $inputFileRelativePath); + $outputFileRelativePathParts = explode('/', $outputFileRelativePath); + + $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { + $fullMatch = $matches[0]; + $inputUrl = $matches[1]; + + if (preg_match('/https?:\/\//is', $inputUrl)) { + return $fullMatch; + } + + $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..'); + $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts); + + if (strpos($inputUrl, '/') !== false) { + $inputUrlParts = explode('/', $inputUrl); + foreach ($inputUrlParts as $key => $inputUrlPart) { + if ($inputUrlPart == '..') { + array_pop($outputUrlParts); + unset($inputUrlParts[$key]); + } + } + $outputUrlParts[] = implode('/', $inputUrlParts); + } else { + $outputUrlParts[] = $inputUrl; + } + $outputUrl = implode('/', $outputUrlParts); + + return str_replace($inputUrl, $outputUrl, $fullMatch); + }; + + $cssContent = preg_replace_callback('/url\(["\']?([^)^"^\']*)["\']?\)/is', $callback, $cssContent); + + return $cssContent; + } + + /** + * Creates template of configuration file for [[actionCompress]]. + * @param string $configFile output file name. + * @throws \yii\console\Exception on failure. + */ + public function actionTemplate($configFile) + { + $template = << [ - // 'yii\web\YiiAsset', - // 'yii\web\JqueryAsset', - ], - // Asset bundle for compression output: - 'targets' => [ - 'app\assets\AllAsset' => [ - 'basePath' => 'path/to/web', - 'baseUrl' => '', - 'js' => 'js/all-{ts}.js', - 'css' => 'css/all-{ts}.css', - ], - ], - // Asset manager configuration: - 'assetManager' => [ - 'basePath' => __DIR__, - 'baseUrl' => '', - ], + // The list of asset bundles to compress: + 'bundles' => [ + // 'yii\web\YiiAsset', + // 'yii\web\JqueryAsset', + ], + // Asset bundle for compression output: + 'targets' => [ + 'app\assets\AllAsset' => [ + 'basePath' => 'path/to/web', + 'baseUrl' => '', + 'js' => 'js/all-{ts}.js', + 'css' => 'css/all-{ts}.css', + ], + ], + // Asset manager configuration: + 'assetManager' => [ + 'basePath' => __DIR__, + 'baseUrl' => '', + ], ]; EOD; - if (file_exists($configFile)) { - if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) { - return; - } - } - if (!file_put_contents($configFile, $template)) { - throw new Exception("Unable to write template file '{$configFile}'."); - } else { - echo "Configuration file template created at '{$configFile}'.\n\n"; - } - } + if (file_exists($configFile)) { + if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) { + return; + } + } + if (!file_put_contents($configFile, $template)) { + throw new Exception("Unable to write template file '{$configFile}'."); + } else { + echo "Configuration file template created at '{$configFile}'.\n\n"; + } + } } diff --git a/framework/console/controllers/CacheController.php b/framework/console/controllers/CacheController.php index 1eebc5ad549..0fcc047fa53 100644 --- a/framework/console/controllers/CacheController.php +++ b/framework/console/controllers/CacheController.php @@ -20,48 +20,48 @@ */ class CacheController extends Controller { - /** - * Lists the caches that can be flushed. - */ - public function actionIndex() - { - $caches = []; - $components = Yii::$app->getComponents(); - foreach ($components as $name => $component) { - if ($component instanceof Cache) { - $caches[$name] = get_class($component); - } elseif (is_array($component) && isset($component['class']) && strpos($component['class'], 'Cache') !== false) { - $caches[$name] = $component['class']; - } - } - if (!empty($caches)) { - echo "The following caches can be flushed:\n\n"; - foreach ($caches as $name => $class) { - echo " * $name: $class\n"; - } - } else { - echo "No cache is used.\n"; - } - } + /** + * Lists the caches that can be flushed. + */ + public function actionIndex() + { + $caches = []; + $components = Yii::$app->getComponents(); + foreach ($components as $name => $component) { + if ($component instanceof Cache) { + $caches[$name] = get_class($component); + } elseif (is_array($component) && isset($component['class']) && strpos($component['class'], 'Cache') !== false) { + $caches[$name] = $component['class']; + } + } + if (!empty($caches)) { + echo "The following caches can be flushed:\n\n"; + foreach ($caches as $name => $class) { + echo " * $name: $class\n"; + } + } else { + echo "No cache is used.\n"; + } + } - /** - * Flushes cache. - * @param string $component Name of the cache application component to use. - * - * @throws \yii\console\Exception - */ - public function actionFlush($component = 'cache') - { - /** @var Cache $cache */ - $cache = Yii::$app->getComponent($component); - if (!$cache || !$cache instanceof Cache) { - throw new Exception('Application component "'.$component.'" is not defined or not a cache.'); - } + /** + * Flushes cache. + * @param string $component Name of the cache application component to use. + * + * @throws \yii\console\Exception + */ + public function actionFlush($component = 'cache') + { + /** @var Cache $cache */ + $cache = Yii::$app->getComponent($component); + if (!$cache || !$cache instanceof Cache) { + throw new Exception('Application component "'.$component.'" is not defined or not a cache.'); + } - if (!$cache->flush()) { - throw new Exception('Unable to flush cache.'); - } + if (!$cache->flush()) { + throw new Exception('Unable to flush cache.'); + } - echo "\nDone.\n"; - } + echo "\nDone.\n"; + } } diff --git a/framework/console/controllers/FixtureController.php b/framework/console/controllers/FixtureController.php index 24989ce0135..4a21054b177 100644 --- a/framework/console/controllers/FixtureController.php +++ b/framework/console/controllers/FixtureController.php @@ -34,288 +34,288 @@ class FixtureController extends Controller { - use FixtureTrait; - - /** - * type of fixture apply to database - */ - const APPLY_ALL = 'all'; - - /** - * @var string controller default action ID. - */ - public $defaultAction = 'load'; - /** - * @var string default namespace to search fixtures in - */ - public $namespace = 'tests\unit\fixtures'; - /** - * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture` - * that disables and enables integrity check, so your data can be safely loaded. - */ - public $globalFixtures = [ - 'yii\test\InitDb', - ]; - - /** - * Returns the names of the global options for this command. - * @return array the names of the global options for this command. - */ - public function options($id) - { - return array_merge(parent::options($id), [ - 'namespace', 'globalFixtures' - ]); - } - - /** - * Loads given fixture. You can load several fixtures specifying - * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no - * whitespace between names. Note that if you are loading fixtures to storage, for example: database or nosql, - * storage will not be cleared, data will be appended to already existed. - * @param array $fixtures - * @param array $except - * @throws \yii\console\Exception - */ - public function actionLoad(array $fixtures, array $except = []) - { - $foundFixtures = $this->findFixtures($fixtures); - - if (!$this->needToApplyAll($fixtures[0])) { - $notFoundFixtures = array_diff($fixtures, $foundFixtures); - - if ($notFoundFixtures) { - $this->notifyNotFound($notFoundFixtures); - } - } - - if (!$foundFixtures) { - throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n" - . "Check that files with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"." - ); - } - - if (!$this->confirmLoad($foundFixtures, $except)) { - return; - } - - $filtered = array_diff($foundFixtures, $except); - $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $filtered)); - - if (!$fixtures) { - throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . ''); - } - - $fixturesObjects = $this->createFixtures($fixtures); - $this->unloadFixtures($fixturesObjects); - $this->loadFixtures($fixturesObjects); - $this->notifyLoaded($fixtures); - } - - /** - * Unloads given fixtures. You can clear environment and unload multiple fixtures by specifying - * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no - * whitespace between names. - * @param array|string $fixtures - * @param array|string $except - */ - public function actionUnload(array $fixtures, array $except = []) - { - $foundFixtures = $this->findFixtures($fixtures); - - if (!$this->needToApplyAll($fixtures[0])) { - $notFoundFixtures = array_diff($fixtures, $foundFixtures); - - if ($notFoundFixtures) { - $this->notifyNotFound($notFoundFixtures); - } - } - - if (!$foundFixtures) { - throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n" - . "Check that fixtures with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"." - ); - } - - if (!$this->confirmUnload($foundFixtures, $except)) { - return; - } - - $filtered = array_diff($foundFixtures, $except); - $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $filtered)); - - if (!$fixtures) { - throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".'); - } - - $this->unloadFixtures($this->createFixtures($fixtures)); - $this->notifyUnloaded($fixtures); - } - - /** - * Notifies user that fixtures were successfully loaded. - * @param array $fixtures - */ - private function notifyLoaded($fixtures) - { - $this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW); - $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN); - $this->outputList($fixtures); - } - - /** - * Notifies user that fixtures were successfully unloaded. - * @param array $fixtures - */ - private function notifyUnloaded($fixtures) - { - $this->stdout("Fixtures were successfully unloaded from namespace:\n", Console::FG_YELLOW); - $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN); - $this->outputList($fixtures); - } - - /** - * Notifies user that fixtures were not found under fixtures path. - * @param array $fixtures - */ - private function notifyNotFound($fixtures) - { - $this->stdout("Some fixtures were not found under path:\n", Console::BG_RED); - $this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN); - $this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED); - $this->outputList($fixtures); - $this->stdout("\n"); - } - - /** - * Prompts user with confirmation if fixtures should be loaded. - * @param array $fixtures - * @param array $except - * @return boolean - */ - private function confirmLoad($fixtures, $except) - { - $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW); - $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN); - - if (count($this->globalFixtures)) { - $this->stdout("Global fixtures will be loaded:\n\n", Console::FG_YELLOW); - $this->outputList($this->globalFixtures); - } - - $this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW); - $this->outputList($fixtures); - - if (count($except)) { - $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW); - $this->outputList($except); - } - - return $this->confirm("\nLoad above fixtures?"); - } - - /** - * Prompts user with confirmation for fixtures that should be unloaded. - * @param array $fixtures - * @param array $except - * @return boolean - */ - private function confirmUnload($fixtures, $except) - { - $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW); - $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN); - - if (count($this->globalFixtures)) { - $this->stdout("Global fixtures will be unloaded:\n\n", Console::FG_YELLOW); - $this->outputList($this->globalFixtures); - } - - $this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW); - $this->outputList($fixtures); - - if (count($except)) { - $this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW); - $this->outputList($except); - } - - return $this->confirm("\nUnload fixtures?"); - } - - /** - * Outputs data to the console as a list. - * @param array $data - */ - private function outputList($data) - { - foreach ($data as $index => $item) { - $this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN); - } - } - - /** - * Checks if needed to apply all fixtures. - * @param string $fixture - * @return bool - */ - public function needToApplyAll($fixture) - { - return $fixture == self::APPLY_ALL; - } - - /** - * @param array $fixtures - * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists. - */ - private function findFixtures(array $fixtures) - { - $fixturesPath = $this->getFixturePath(); - - $filesToSearch = ['*Fixture.php']; - if (!$this->needToApplyAll($fixtures[0])) { - $filesToSearch = []; - foreach ($fixtures as $fileName) { - $filesToSearch[] = $fileName . 'Fixture.php'; - } - } - - $files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]); - $foundFixtures = []; - - foreach ($files as $fixture) { - $foundFixtures[] = basename($fixture, 'Fixture.php'); - } - - return $foundFixtures; - } - - /** - * Returns valid fixtures config that can be used to load them. - * @param array $fixtures fixtures to configure - * @return array - */ - private function getFixturesConfig($fixtures) - { - $config = []; - - foreach ($fixtures as $fixture) { - - $isNamespaced = (strpos($fixture, '\\') !== false); - $fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture'; - - if (class_exists($fullClassName)) { - $config[] = $fullClassName; - } - } - - return $config; - } - - /** - * Returns fixture path that determined on fixtures namespace. - * @return string fixture path - */ - private function getFixturePath() - { - return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace)); - } + use FixtureTrait; + + /** + * type of fixture apply to database + */ + const APPLY_ALL = 'all'; + + /** + * @var string controller default action ID. + */ + public $defaultAction = 'load'; + /** + * @var string default namespace to search fixtures in + */ + public $namespace = 'tests\unit\fixtures'; + /** + * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture` + * that disables and enables integrity check, so your data can be safely loaded. + */ + public $globalFixtures = [ + 'yii\test\InitDb', + ]; + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function options($id) + { + return array_merge(parent::options($id), [ + 'namespace', 'globalFixtures' + ]); + } + + /** + * Loads given fixture. You can load several fixtures specifying + * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no + * whitespace between names. Note that if you are loading fixtures to storage, for example: database or nosql, + * storage will not be cleared, data will be appended to already existed. + * @param array $fixtures + * @param array $except + * @throws \yii\console\Exception + */ + public function actionLoad(array $fixtures, array $except = []) + { + $foundFixtures = $this->findFixtures($fixtures); + + if (!$this->needToApplyAll($fixtures[0])) { + $notFoundFixtures = array_diff($fixtures, $foundFixtures); + + if ($notFoundFixtures) { + $this->notifyNotFound($notFoundFixtures); + } + } + + if (!$foundFixtures) { + throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n" + . "Check that files with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"." + ); + } + + if (!$this->confirmLoad($foundFixtures, $except)) { + return; + } + + $filtered = array_diff($foundFixtures, $except); + $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $filtered)); + + if (!$fixtures) { + throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . ''); + } + + $fixturesObjects = $this->createFixtures($fixtures); + $this->unloadFixtures($fixturesObjects); + $this->loadFixtures($fixturesObjects); + $this->notifyLoaded($fixtures); + } + + /** + * Unloads given fixtures. You can clear environment and unload multiple fixtures by specifying + * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no + * whitespace between names. + * @param array|string $fixtures + * @param array|string $except + */ + public function actionUnload(array $fixtures, array $except = []) + { + $foundFixtures = $this->findFixtures($fixtures); + + if (!$this->needToApplyAll($fixtures[0])) { + $notFoundFixtures = array_diff($fixtures, $foundFixtures); + + if ($notFoundFixtures) { + $this->notifyNotFound($notFoundFixtures); + } + } + + if (!$foundFixtures) { + throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n" + . "Check that fixtures with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"." + ); + } + + if (!$this->confirmUnload($foundFixtures, $except)) { + return; + } + + $filtered = array_diff($foundFixtures, $except); + $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures, $filtered)); + + if (!$fixtures) { + throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".'); + } + + $this->unloadFixtures($this->createFixtures($fixtures)); + $this->notifyUnloaded($fixtures); + } + + /** + * Notifies user that fixtures were successfully loaded. + * @param array $fixtures + */ + private function notifyLoaded($fixtures) + { + $this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW); + $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN); + $this->outputList($fixtures); + } + + /** + * Notifies user that fixtures were successfully unloaded. + * @param array $fixtures + */ + private function notifyUnloaded($fixtures) + { + $this->stdout("Fixtures were successfully unloaded from namespace:\n", Console::FG_YELLOW); + $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN); + $this->outputList($fixtures); + } + + /** + * Notifies user that fixtures were not found under fixtures path. + * @param array $fixtures + */ + private function notifyNotFound($fixtures) + { + $this->stdout("Some fixtures were not found under path:\n", Console::BG_RED); + $this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN); + $this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED); + $this->outputList($fixtures); + $this->stdout("\n"); + } + + /** + * Prompts user with confirmation if fixtures should be loaded. + * @param array $fixtures + * @param array $except + * @return boolean + */ + private function confirmLoad($fixtures, $except) + { + $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW); + $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN); + + if (count($this->globalFixtures)) { + $this->stdout("Global fixtures will be loaded:\n\n", Console::FG_YELLOW); + $this->outputList($this->globalFixtures); + } + + $this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW); + $this->outputList($fixtures); + + if (count($except)) { + $this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW); + $this->outputList($except); + } + + return $this->confirm("\nLoad above fixtures?"); + } + + /** + * Prompts user with confirmation for fixtures that should be unloaded. + * @param array $fixtures + * @param array $except + * @return boolean + */ + private function confirmUnload($fixtures, $except) + { + $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW); + $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN); + + if (count($this->globalFixtures)) { + $this->stdout("Global fixtures will be unloaded:\n\n", Console::FG_YELLOW); + $this->outputList($this->globalFixtures); + } + + $this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW); + $this->outputList($fixtures); + + if (count($except)) { + $this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW); + $this->outputList($except); + } + + return $this->confirm("\nUnload fixtures?"); + } + + /** + * Outputs data to the console as a list. + * @param array $data + */ + private function outputList($data) + { + foreach ($data as $index => $item) { + $this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN); + } + } + + /** + * Checks if needed to apply all fixtures. + * @param string $fixture + * @return bool + */ + public function needToApplyAll($fixture) + { + return $fixture == self::APPLY_ALL; + } + + /** + * @param array $fixtures + * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists. + */ + private function findFixtures(array $fixtures) + { + $fixturesPath = $this->getFixturePath(); + + $filesToSearch = ['*Fixture.php']; + if (!$this->needToApplyAll($fixtures[0])) { + $filesToSearch = []; + foreach ($fixtures as $fileName) { + $filesToSearch[] = $fileName . 'Fixture.php'; + } + } + + $files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]); + $foundFixtures = []; + + foreach ($files as $fixture) { + $foundFixtures[] = basename($fixture, 'Fixture.php'); + } + + return $foundFixtures; + } + + /** + * Returns valid fixtures config that can be used to load them. + * @param array $fixtures fixtures to configure + * @return array + */ + private function getFixturesConfig($fixtures) + { + $config = []; + + foreach ($fixtures as $fixture) { + + $isNamespaced = (strpos($fixture, '\\') !== false); + $fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture'; + + if (class_exists($fullClassName)) { + $config[] = $fullClassName; + } + } + + return $config; + } + + /** + * Returns fixture path that determined on fixtures namespace. + * @return string fixture path + */ + private function getFixturePath() + { + return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace)); + } } diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 99a5846c066..694113c864f 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -38,434 +38,441 @@ */ class HelpController extends Controller { - /** - * Displays available commands or the detailed information - * about a particular command. For example, - * - * @param string $command The name of the command to show help about. - * If not provided, all available commands will be displayed. - * @return integer the exit status - * @throws Exception if the command for help is unknown - */ - public function actionIndex($command = null) - { - if ($command !== null) { - $result = Yii::$app->createController($command); - if ($result === false) { - throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', [ - 'command' => $this->ansiFormat($command, Console::FG_YELLOW), - ])); - } - - list($controller, $actionID) = $result; - - $actions = $this->getActions($controller); - if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) { - $this->getActionHelp($controller, $actionID); - } else { - $this->getControllerHelp($controller); - } - } else { - $this->getHelp(); - } - } - - /** - * Returns all available command names. - * @return array all available command names - */ - public function getCommands() - { - $commands = $this->getModuleCommands(Yii::$app); - sort($commands); - return array_unique($commands); - } - - /** - * Returns an array of commands an their descriptions. - * @return array all available commands as keys and their description as values. - */ - protected function getCommandDescriptions() - { - $descriptions = []; - foreach ($this->getCommands() as $command) { - $description = ''; - - $result = Yii::$app->createController($command); - if ($result !== false) { - list($controller, $actionID) = $result; - $class = new \ReflectionClass($controller); - - $docLines = preg_split('~(\n|\r|\r\n)~', $class->getDocComment()); - if (isset($docLines[1])) { - $description = trim($docLines[1], ' *'); - } - } - - $descriptions[$command] = $description; - } - return $descriptions; - } - - /** - * Returns all available actions of the specified controller. - * @param Controller $controller the controller instance - * @return array all available action IDs. - */ - public function getActions($controller) - { - $actions = array_keys($controller->actions()); - $class = new \ReflectionClass($controller); - foreach ($class->getMethods() as $method) { - $name = $method->getName(); - if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { - $actions[] = Inflector::camel2id(substr($name, 6)); - } - } - sort($actions); - return array_unique($actions); - } - - /** - * Returns available commands of a specified module. - * @param \yii\base\Module $module the module instance - * @return array the available command names - */ - protected function getModuleCommands($module) - { - $prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/'; - - $commands = []; - foreach (array_keys($module->controllerMap) as $id) { - $commands[] = $prefix . $id; - } - - foreach ($module->getModules() as $id => $child) { - if (($child = $module->getModule($id)) === null) { - continue; - } - foreach ($this->getModuleCommands($child) as $command) { - $commands[] = $command; - } - } - - $controllerPath = $module->getControllerPath(); - if (is_dir($controllerPath)) { - $files = scandir($controllerPath); - foreach ($files as $file) { - if (strcmp(substr($file, -14), 'Controller.php') === 0) { - $commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14)); - } - } - } - - return $commands; - } - - /** - * Displays all available commands. - */ - protected function getHelp() - { - $commands = $this->getCommandDescriptions(); - if (!empty($commands)) { - $this->stdout("\nThe following commands are available:\n\n", Console::BOLD); - $len = 0; - foreach ($commands as $command => $description) { - if (($l = strlen($command)) > $len) { - $len = $l; - } - } - foreach ($commands as $command => $description) { - echo "- " . $this->ansiFormat($command, Console::FG_YELLOW); - echo str_repeat(' ', $len + 3 - strlen($command)) . $description; - echo "\n"; - } - $scriptName = $this->getScriptName(); - $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD); - echo "\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' - . $this->ansiFormat('', Console::FG_CYAN) . "\n\n"; - } else { - $this->stdout("\nNo commands are found.\n\n", Console::BOLD); - } - } - - /** - * Displays the overall information of the command. - * @param Controller $controller the controller instance - */ - protected function getControllerHelp($controller) - { - $class = new \ReflectionClass($controller); - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($class->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $comment = trim(substr($comment, 0, $matches[0][1])); - } - - if ($comment !== '') { - $this->stdout("\nDESCRIPTION\n", Console::BOLD); - echo "\n" . Console::renderColoredString($comment) . "\n\n"; - } - - $actions = $this->getActions($controller); - if (!empty($actions)) { - $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD); - $prefix = $controller->getUniqueId(); - foreach ($actions as $action) { - echo '- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW); - if ($action === $controller->defaultAction) { - $this->stdout(' (default)', Console::FG_GREEN); - } - $summary = $this->getActionSummary($controller, $action); - if ($summary !== '') { - echo ': ' . $summary; - } - echo "\n"; - } - $scriptName = $this->getScriptName(); - echo "\nTo see the detailed information about individual sub-commands, enter:\n"; - echo "\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' - . $this->ansiFormat('', Console::FG_CYAN) . "\n\n"; - } - } - - /** - * Returns the short summary of the action. - * @param Controller $controller the controller instance - * @param string $actionID action ID - * @return string the summary about the action - */ - protected function getActionSummary($controller, $actionID) - { - $action = $controller->createAction($actionID); - if ($action === null) { - return ''; - } - if ($action instanceof InlineAction) { - $reflection = new \ReflectionMethod($controller, $action->actionMethod); - } else { - $reflection = new \ReflectionClass($action); - } - $tags = $this->parseComment($reflection->getDocComment()); - if ($tags['description'] !== '') { - $limit = 73 - strlen($action->getUniqueId()); - if ($actionID === $controller->defaultAction) { - $limit -= 10; - } - if ($limit < 0) { - $limit = 50; - } - $description = $tags['description']; - if (($pos = strpos($tags['description'], "\n")) !== false) { - $description = substr($description, 0, $pos); - } - $text = substr($description, 0, $limit); - return strlen($description) > $limit ? $text . '...' : $text; - } else { - return ''; - } - } - - /** - * Displays the detailed information of a command action. - * @param Controller $controller the controller instance - * @param string $actionID action ID - * @throws Exception if the action does not exist - */ - protected function getActionHelp($controller, $actionID) - { - $action = $controller->createAction($actionID); - if ($action === null) { - throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', [ - 'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), - ])); - } - if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($controller, $action->actionMethod); - } else { - $method = new \ReflectionMethod($action, 'run'); - } - - $tags = $this->parseComment($method->getDocComment()); - $options = $this->getOptionHelps($controller, $actionID); - - if ($tags['description'] !== '') { - $this->stdout("\nDESCRIPTION\n", Console::BOLD); - echo "\n" . Console::renderColoredString($tags['description']) . "\n\n"; - } - - $this->stdout("\nUSAGE\n\n", Console::BOLD); - $scriptName = $this->getScriptName(); - if ($action->id === $controller->defaultAction) { - echo $scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW); - } else { - echo $scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW); - } - list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : []); - foreach ($required as $arg => $description) { - $this->stdout(' <' . $arg . '>', Console::FG_CYAN); - } - foreach ($optional as $arg => $description) { - $this->stdout(' [' . $arg . ']', Console::FG_CYAN); - } - if (!empty($options)) { - $this->stdout(' [...options...]', Console::FG_RED); - } - echo "\n\n"; - - if (!empty($required) || !empty($optional)) { - echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; - } - - if (!empty($options)) { - $this->stdout("\nOPTIONS\n\n", Console::BOLD); - echo implode("\n\n", $options) . "\n\n"; - } - } - - /** - * Returns the help information about arguments. - * @param \ReflectionMethod $method - * @param string $tags the parsed comment block related with arguments - * @return array the required and optional argument help information - */ - protected function getArgHelps($method, $tags) - { - if (is_string($tags)) { - $tags = [$tags]; - } - $params = $method->getParameters(); - $optional = $required = []; - foreach ($params as $i => $param) { - $name = $param->getName(); - $tag = isset($tags[$i]) ? $tags[$i] : ''; - if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { - $type = $matches[1]; - $comment = $matches[3]; - } else { - $type = null; - $comment = $tag; - } - if ($param->isDefaultValueAvailable()) { - $optional[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), false, $type, $param->getDefaultValue(), $comment); - } else { - $required[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), true, $type, null, $comment); - } - } - - return [$required, $optional]; - } - - /** - * Returns the help information about the options available for a console controller. - * @param Controller $controller the console controller - * @param string $actionID name of the action, if set include local options for that action - * @return array the help information about the options - */ - protected function getOptionHelps($controller, $actionID) - { - $optionNames = $controller->options($actionID); - if (empty($optionNames)) { - return []; - } - - $class = new \ReflectionClass($controller); - $options = []; - foreach ($class->getProperties() as $property) { - $name = $property->getName(); - if (!in_array($name, $optionNames, true)) { - continue; - } - $defaultValue = $property->getValue($controller); - $tags = $this->parseComment($property->getDocComment()); - if (isset($tags['var']) || isset($tags['property'])) { - $doc = isset($tags['var']) ? $tags['var'] : $tags['property']; - if (is_array($doc)) { - $doc = reset($doc); - } - if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) { - $type = $matches[1]; - $comment = $matches[2]; - } else { - $type = null; - $comment = $doc; - } - $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, $type, $defaultValue, $comment); - } else { - $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, null, $defaultValue, ''); - } - } - ksort($options); - return $options; - } - - /** - * Parses the comment block into tags. - * @param string $comment the comment block - * @return array the parsed tags - */ - protected function parseComment($comment) - { - $tags = []; - $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); - $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); - foreach ($parts as $part) { - if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { - $name = $matches[1]; - if (!isset($tags[$name])) { - $tags[$name] = trim($matches[2]); - } elseif (is_array($tags[$name])) { - $tags[$name][] = trim($matches[2]); - } else { - $tags[$name] = [$tags[$name], trim($matches[2])]; - } - } - } - return $tags; - } - - /** - * Generates a well-formed string for an argument or option. - * @param string $name the name of the argument or option - * @param boolean $required whether the argument is required - * @param string $type the type of the option or argument - * @param mixed $defaultValue the default value of the option or argument - * @param string $comment comment about the option or argument - * @return string the formatted string for the argument or option - */ - protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) - { - $doc = ''; - $comment = trim($comment); - - if ($defaultValue !== null && !is_array($defaultValue)) { - if ($type === null) { - $type = gettype($defaultValue); - } - if (is_bool($defaultValue)) { - // show as integer to avoid confusion - $defaultValue = (int)$defaultValue; - } - $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; - } elseif (trim($type) !== '') { - $doc = $type; - } - - if ($doc === '') { - $doc = $comment; - } elseif ($comment !== '') { - $doc .= "\n" . preg_replace("/^/m", " ", $comment); - } - - $name = $required ? "$name (required)" : $name; - return $doc === '' ? $name : "$name: $doc"; - } - - /** - * @return string the name of the cli script currently running. - */ - protected function getScriptName() - { - return basename(Yii::$app->request->scriptFile); - } + /** + * Displays available commands or the detailed information + * about a particular command. For example, + * + * @param string $command The name of the command to show help about. + * If not provided, all available commands will be displayed. + * @return integer the exit status + * @throws Exception if the command for help is unknown + */ + public function actionIndex($command = null) + { + if ($command !== null) { + $result = Yii::$app->createController($command); + if ($result === false) { + throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', [ + 'command' => $this->ansiFormat($command, Console::FG_YELLOW), + ])); + } + + list($controller, $actionID) = $result; + + $actions = $this->getActions($controller); + if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) { + $this->getActionHelp($controller, $actionID); + } else { + $this->getControllerHelp($controller); + } + } else { + $this->getHelp(); + } + } + + /** + * Returns all available command names. + * @return array all available command names + */ + public function getCommands() + { + $commands = $this->getModuleCommands(Yii::$app); + sort($commands); + + return array_unique($commands); + } + + /** + * Returns an array of commands an their descriptions. + * @return array all available commands as keys and their description as values. + */ + protected function getCommandDescriptions() + { + $descriptions = []; + foreach ($this->getCommands() as $command) { + $description = ''; + + $result = Yii::$app->createController($command); + if ($result !== false) { + list($controller, $actionID) = $result; + $class = new \ReflectionClass($controller); + + $docLines = preg_split('~(\n|\r|\r\n)~', $class->getDocComment()); + if (isset($docLines[1])) { + $description = trim($docLines[1], ' *'); + } + } + + $descriptions[$command] = $description; + } + + return $descriptions; + } + + /** + * Returns all available actions of the specified controller. + * @param Controller $controller the controller instance + * @return array all available action IDs. + */ + public function getActions($controller) + { + $actions = array_keys($controller->actions()); + $class = new \ReflectionClass($controller); + foreach ($class->getMethods() as $method) { + $name = $method->getName(); + if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { + $actions[] = Inflector::camel2id(substr($name, 6)); + } + } + sort($actions); + + return array_unique($actions); + } + + /** + * Returns available commands of a specified module. + * @param \yii\base\Module $module the module instance + * @return array the available command names + */ + protected function getModuleCommands($module) + { + $prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/'; + + $commands = []; + foreach (array_keys($module->controllerMap) as $id) { + $commands[] = $prefix . $id; + } + + foreach ($module->getModules() as $id => $child) { + if (($child = $module->getModule($id)) === null) { + continue; + } + foreach ($this->getModuleCommands($child) as $command) { + $commands[] = $command; + } + } + + $controllerPath = $module->getControllerPath(); + if (is_dir($controllerPath)) { + $files = scandir($controllerPath); + foreach ($files as $file) { + if (strcmp(substr($file, -14), 'Controller.php') === 0) { + $commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14)); + } + } + } + + return $commands; + } + + /** + * Displays all available commands. + */ + protected function getHelp() + { + $commands = $this->getCommandDescriptions(); + if (!empty($commands)) { + $this->stdout("\nThe following commands are available:\n\n", Console::BOLD); + $len = 0; + foreach ($commands as $command => $description) { + if (($l = strlen($command)) > $len) { + $len = $l; + } + } + foreach ($commands as $command => $description) { + echo "- " . $this->ansiFormat($command, Console::FG_YELLOW); + echo str_repeat(' ', $len + 3 - strlen($command)) . $description; + echo "\n"; + } + $scriptName = $this->getScriptName(); + $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD); + echo "\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' + . $this->ansiFormat('', Console::FG_CYAN) . "\n\n"; + } else { + $this->stdout("\nNo commands are found.\n\n", Console::BOLD); + } + } + + /** + * Displays the overall information of the command. + * @param Controller $controller the controller instance + */ + protected function getControllerHelp($controller) + { + $class = new \ReflectionClass($controller); + $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($class->getDocComment(), '/'))), "\r", ''); + if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { + $comment = trim(substr($comment, 0, $matches[0][1])); + } + + if ($comment !== '') { + $this->stdout("\nDESCRIPTION\n", Console::BOLD); + echo "\n" . Console::renderColoredString($comment) . "\n\n"; + } + + $actions = $this->getActions($controller); + if (!empty($actions)) { + $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD); + $prefix = $controller->getUniqueId(); + foreach ($actions as $action) { + echo '- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW); + if ($action === $controller->defaultAction) { + $this->stdout(' (default)', Console::FG_GREEN); + } + $summary = $this->getActionSummary($controller, $action); + if ($summary !== '') { + echo ': ' . $summary; + } + echo "\n"; + } + $scriptName = $this->getScriptName(); + echo "\nTo see the detailed information about individual sub-commands, enter:\n"; + echo "\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' + . $this->ansiFormat('', Console::FG_CYAN) . "\n\n"; + } + } + + /** + * Returns the short summary of the action. + * @param Controller $controller the controller instance + * @param string $actionID action ID + * @return string the summary about the action + */ + protected function getActionSummary($controller, $actionID) + { + $action = $controller->createAction($actionID); + if ($action === null) { + return ''; + } + if ($action instanceof InlineAction) { + $reflection = new \ReflectionMethod($controller, $action->actionMethod); + } else { + $reflection = new \ReflectionClass($action); + } + $tags = $this->parseComment($reflection->getDocComment()); + if ($tags['description'] !== '') { + $limit = 73 - strlen($action->getUniqueId()); + if ($actionID === $controller->defaultAction) { + $limit -= 10; + } + if ($limit < 0) { + $limit = 50; + } + $description = $tags['description']; + if (($pos = strpos($tags['description'], "\n")) !== false) { + $description = substr($description, 0, $pos); + } + $text = substr($description, 0, $limit); + + return strlen($description) > $limit ? $text . '...' : $text; + } else { + return ''; + } + } + + /** + * Displays the detailed information of a command action. + * @param Controller $controller the controller instance + * @param string $actionID action ID + * @throws Exception if the action does not exist + */ + protected function getActionHelp($controller, $actionID) + { + $action = $controller->createAction($actionID); + if ($action === null) { + throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', [ + 'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), + ])); + } + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($controller, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $tags = $this->parseComment($method->getDocComment()); + $options = $this->getOptionHelps($controller, $actionID); + + if ($tags['description'] !== '') { + $this->stdout("\nDESCRIPTION\n", Console::BOLD); + echo "\n" . Console::renderColoredString($tags['description']) . "\n\n"; + } + + $this->stdout("\nUSAGE\n\n", Console::BOLD); + $scriptName = $this->getScriptName(); + if ($action->id === $controller->defaultAction) { + echo $scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW); + } else { + echo $scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW); + } + list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : []); + foreach ($required as $arg => $description) { + $this->stdout(' <' . $arg . '>', Console::FG_CYAN); + } + foreach ($optional as $arg => $description) { + $this->stdout(' [' . $arg . ']', Console::FG_CYAN); + } + if (!empty($options)) { + $this->stdout(' [...options...]', Console::FG_RED); + } + echo "\n\n"; + + if (!empty($required) || !empty($optional)) { + echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; + } + + if (!empty($options)) { + $this->stdout("\nOPTIONS\n\n", Console::BOLD); + echo implode("\n\n", $options) . "\n\n"; + } + } + + /** + * Returns the help information about arguments. + * @param \ReflectionMethod $method + * @param string $tags the parsed comment block related with arguments + * @return array the required and optional argument help information + */ + protected function getArgHelps($method, $tags) + { + if (is_string($tags)) { + $tags = [$tags]; + } + $params = $method->getParameters(); + $optional = $required = []; + foreach ($params as $i => $param) { + $name = $param->getName(); + $tag = isset($tags[$i]) ? $tags[$i] : ''; + if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { + $type = $matches[1]; + $comment = $matches[3]; + } else { + $type = null; + $comment = $tag; + } + if ($param->isDefaultValueAvailable()) { + $optional[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), false, $type, $param->getDefaultValue(), $comment); + } else { + $required[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), true, $type, null, $comment); + } + } + + return [$required, $optional]; + } + + /** + * Returns the help information about the options available for a console controller. + * @param Controller $controller the console controller + * @param string $actionID name of the action, if set include local options for that action + * @return array the help information about the options + */ + protected function getOptionHelps($controller, $actionID) + { + $optionNames = $controller->options($actionID); + if (empty($optionNames)) { + return []; + } + + $class = new \ReflectionClass($controller); + $options = []; + foreach ($class->getProperties() as $property) { + $name = $property->getName(); + if (!in_array($name, $optionNames, true)) { + continue; + } + $defaultValue = $property->getValue($controller); + $tags = $this->parseComment($property->getDocComment()); + if (isset($tags['var']) || isset($tags['property'])) { + $doc = isset($tags['var']) ? $tags['var'] : $tags['property']; + if (is_array($doc)) { + $doc = reset($doc); + } + if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) { + $type = $matches[1]; + $comment = $matches[2]; + } else { + $type = null; + $comment = $doc; + } + $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, $type, $defaultValue, $comment); + } else { + $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, null, $defaultValue, ''); + } + } + ksort($options); + + return $options; + } + + /** + * Parses the comment block into tags. + * @param string $comment the comment block + * @return array the parsed tags + */ + protected function parseComment($comment) + { + $tags = []; + $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); + $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { + $name = $matches[1]; + if (!isset($tags[$name])) { + $tags[$name] = trim($matches[2]); + } elseif (is_array($tags[$name])) { + $tags[$name][] = trim($matches[2]); + } else { + $tags[$name] = [$tags[$name], trim($matches[2])]; + } + } + } + + return $tags; + } + + /** + * Generates a well-formed string for an argument or option. + * @param string $name the name of the argument or option + * @param boolean $required whether the argument is required + * @param string $type the type of the option or argument + * @param mixed $defaultValue the default value of the option or argument + * @param string $comment comment about the option or argument + * @return string the formatted string for the argument or option + */ + protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) + { + $doc = ''; + $comment = trim($comment); + + if ($defaultValue !== null && !is_array($defaultValue)) { + if ($type === null) { + $type = gettype($defaultValue); + } + if (is_bool($defaultValue)) { + // show as integer to avoid confusion + $defaultValue = (int) $defaultValue; + } + $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; + } elseif (trim($type) !== '') { + $doc = $type; + } + + if ($doc === '') { + $doc = $comment; + } elseif ($comment !== '') { + $doc .= "\n" . preg_replace("/^/m", " ", $comment); + } + + $name = $required ? "$name (required)" : $name; + + return $doc === '' ? $name : "$name: $doc"; + } + + /** + * @return string the name of the cli script currently running. + */ + protected function getScriptName() + { + return basename(Yii::$app->request->scriptFile); + } } diff --git a/framework/console/controllers/MessageController.php b/framework/console/controllers/MessageController.php index 456fc51292c..be135bfa5bb 100644 --- a/framework/console/controllers/MessageController.php +++ b/framework/console/controllers/MessageController.php @@ -35,345 +35,345 @@ */ class MessageController extends Controller { - /** - * @var string controller default action ID. - */ - public $defaultAction = 'extract'; + /** + * @var string controller default action ID. + */ + public $defaultAction = 'extract'; + /** + * Creates a configuration file for the "extract" command. + * + * The generated configuration file contains detailed instructions on + * how to customize it to fit for your needs. After customization, + * you may use this configuration file with the "extract" command. + * + * @param string $filePath output file name or alias. + * @throws Exception on failure. + */ + public function actionConfig($filePath) + { + $filePath = Yii::getAlias($filePath); + if (file_exists($filePath)) { + if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) { + return; + } + } + copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath); + echo "Configuration file template created at '{$filePath}'.\n\n"; + } - /** - * Creates a configuration file for the "extract" command. - * - * The generated configuration file contains detailed instructions on - * how to customize it to fit for your needs. After customization, - * you may use this configuration file with the "extract" command. - * - * @param string $filePath output file name or alias. - * @throws Exception on failure. - */ - public function actionConfig($filePath) - { - $filePath = Yii::getAlias($filePath); - if (file_exists($filePath)) { - if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) { - return; - } - } - copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath); - echo "Configuration file template created at '{$filePath}'.\n\n"; - } + /** + * Extracts messages to be translated from source code. + * + * This command will search through source code files and extract + * messages that need to be translated in different languages. + * + * @param string $configFile the path or alias of the configuration file. + * You may use the "yii message/config" command to generate + * this file and then customize it for your needs. + * @throws Exception on failure. + */ + public function actionExtract($configFile) + { + $configFile = Yii::getAlias($configFile); + if (!is_file($configFile)) { + throw new Exception("The configuration file does not exist: $configFile"); + } - /** - * Extracts messages to be translated from source code. - * - * This command will search through source code files and extract - * messages that need to be translated in different languages. - * - * @param string $configFile the path or alias of the configuration file. - * You may use the "yii message/config" command to generate - * this file and then customize it for your needs. - * @throws Exception on failure. - */ - public function actionExtract($configFile) - { - $configFile = Yii::getAlias($configFile); - if (!is_file($configFile)) { - throw new Exception("The configuration file does not exist: $configFile"); - } + $config = array_merge([ + 'translator' => 'Yii::t', + 'overwrite' => false, + 'removeUnused' => false, + 'sort' => false, + 'format' => 'php', + ], require($configFile)); - $config = array_merge([ - 'translator' => 'Yii::t', - 'overwrite' => false, - 'removeUnused' => false, - 'sort' => false, - 'format' => 'php', - ], require($configFile)); + if (!isset($config['sourcePath'], $config['messagePath'], $config['languages'])) { + throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".'); + } + if (!is_dir($config['sourcePath'])) { + throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); + } + if (in_array($config['format'], ['php', 'po'])) { + if (!is_dir($config['messagePath'])) { + throw new Exception("The message path {$config['messagePath']} is not a valid directory."); + } + } + if (empty($config['languages'])) { + throw new Exception("Languages cannot be empty."); + } + if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'])) { + throw new Exception('Format should be either "php", "po" or "db".'); + } - if (!isset($config['sourcePath'], $config['messagePath'], $config['languages'])) { - throw new Exception('The configuration file must specify "sourcePath", "messagePath" and "languages".'); - } - if (!is_dir($config['sourcePath'])) { - throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); - } - if (in_array($config['format'], ['php', 'po'])) { - if (!is_dir($config['messagePath'])) { - throw new Exception("The message path {$config['messagePath']} is not a valid directory."); - } - } - if (empty($config['languages'])) { - throw new Exception("Languages cannot be empty."); - } - if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'])) { - throw new Exception('Format should be either "php", "po" or "db".'); - } + $files = FileHelper::findFiles(realpath($config['sourcePath']), $config); - $files = FileHelper::findFiles(realpath($config['sourcePath']), $config); + $messages = []; + foreach ($files as $file) { + $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator'])); + } + if (in_array($config['format'], ['php', 'po'])) { + foreach ($config['languages'] as $language) { + $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language; + if (!is_dir($dir)) { + @mkdir($dir); + } + foreach ($messages as $category => $msgs) { + $file = str_replace("\\", '/', "$dir/$category." . $config['format']); + $path = dirname($file); + if (!is_dir($path)) { + mkdir($path, 0755, true); + } + $msgs = array_values(array_unique($msgs)); + $this->generateMessageFile($msgs, $file, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['format']); + } + } + } elseif ($config['format'] === 'db') { + $db = \Yii::$app->getComponent(isset($config['db']) ? $config['db'] : 'db'); + if (!$db instanceof \yii\db\Connection) { + throw new Exception('The "db" option must refer to a valid database application component.'); + } + $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}'; + $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}'; + $this->saveMessagesToDb( + $messages, + $db, + $sourceMessageTable, + $messageTable, + $config['removeUnused'], + $config['languages'] + ); + } + } - $messages = []; - foreach ($files as $file) { - $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator'])); - } - if (in_array($config['format'], ['php', 'po'])) { - foreach ($config['languages'] as $language) { - $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language; - if (!is_dir($dir)) { - @mkdir($dir); - } - foreach ($messages as $category => $msgs) { - $file = str_replace("\\", '/', "$dir/$category." . $config['format']); - $path = dirname($file); - if (!is_dir($path)) { - mkdir($path, 0755, true); - } - $msgs = array_values(array_unique($msgs)); - $this->generateMessageFile($msgs, $file, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['format']); - } - } - } elseif ($config['format'] === 'db') { - $db = \Yii::$app->getComponent(isset($config['db']) ? $config['db'] : 'db'); - if (!$db instanceof \yii\db\Connection) { - throw new Exception('The "db" option must refer to a valid database application component.'); - } - $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}'; - $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}'; - $this->saveMessagesToDb( - $messages, - $db, - $sourceMessageTable, - $messageTable, - $config['removeUnused'], - $config['languages'] - ); - } - } + /** + * Saves messages to database + * + * @param array $messages + * @param \yii\db\Connection $db + * @param string $sourceMessageTable + * @param string $messageTable + * @param boolean $removeUnused + * @param array $languages + */ + protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages) + { + $q = new \yii\db\Query; + $current = []; - /** - * Saves messages to database - * - * @param array $messages - * @param \yii\db\Connection $db - * @param string $sourceMessageTable - * @param string $messageTable - * @param boolean $removeUnused - * @param array $languages - */ - protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages) - { - $q = new \yii\db\Query; - $current = []; + foreach ($q->select(['id', 'category', 'message'])->from($sourceMessageTable)->all() as $row) { + $current[$row['category']][$row['id']] = $row['message']; + } - foreach ($q->select(['id', 'category', 'message'])->from($sourceMessageTable)->all() as $row) { - $current[$row['category']][$row['id']] = $row['message']; - } + $new = []; + $obsolete = []; - $new = []; - $obsolete = []; + foreach ($messages as $category => $msgs) { + $msgs = array_unique($msgs); - foreach ($messages as $category => $msgs) { - $msgs = array_unique($msgs); + if (isset($current[$category])) { + $new[$category] = array_diff($msgs, $current[$category]); + $obsolete = array_diff($current[$category], $msgs); + } else { + $new[$category] = $msgs; + } + } - if (isset($current[$category])) { - $new[$category] = array_diff($msgs, $current[$category]); - $obsolete = array_diff($current[$category], $msgs); - } else { - $new[$category] = $msgs; - } - } + foreach (array_diff(array_keys($current), array_keys($messages)) as $category) { + $obsolete += $current[$category]; + } - foreach (array_diff(array_keys($current), array_keys($messages)) as $category) { - $obsolete += $current[$category]; - } + if (!$removeUnused) { + foreach ($obsolete as $pk => $m) { + if (mb_substr($m, 0, 2) === '@@' && mb_substr($m, -2) === '@@') { + unset($obsolete[$pk]); + } + } + } - if (!$removeUnused) { - foreach ($obsolete as $pk => $m) { - if (mb_substr($m, 0, 2) === '@@' && mb_substr($m, -2) === '@@') { - unset($obsolete[$pk]); - } - } - } + $obsolete = array_keys($obsolete); + echo "Inserting new messages..."; + $savedFlag = false; - $obsolete = array_keys($obsolete); - echo "Inserting new messages..."; - $savedFlag = false; + foreach ($new as $category => $msgs) { + foreach ($msgs as $m) { + $savedFlag = true; - foreach ($new as $category => $msgs) { - foreach ($msgs as $m) { - $savedFlag = true; + $db->createCommand() + ->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute(); + $lastId = $db->getLastInsertID(); + foreach ($languages as $language) { + $db->createCommand() + ->insert($messageTable, ['id' => $lastId, 'language' => $language])->execute(); + } + } + } - $db->createCommand() - ->insert($sourceMessageTable, ['category' => $category, 'message' => $m])->execute(); - $lastId = $db->getLastInsertID(); - foreach ($languages as $language) { - $db->createCommand() - ->insert($messageTable, ['id' => $lastId, 'language' => $language])->execute(); - } - } - } + echo $savedFlag ? "saved.\n" : "nothing new...skipped.\n"; + echo $removeUnused ? "Deleting obsoleted messages..." : "Updating obsoleted messages..."; - echo $savedFlag ? "saved.\n" : "nothing new...skipped.\n"; - echo $removeUnused ? "Deleting obsoleted messages..." : "Updating obsoleted messages..."; + if (empty($obsolete)) { + echo "nothing obsoleted...skipped.\n"; + } else { + if ($removeUnused) { + $db->createCommand() + ->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute(); + echo "deleted.\n"; + } else { + $last_id = $db->getLastInsertID(); + $db->createCommand() + ->update( + $sourceMessageTable, + ['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")], + ['in', 'id', $obsolete] + )->execute(); + foreach ($languages as $language) { + $db->createCommand() + ->insert($messageTable, ['id' => $last_id, 'language' => $language])->execute(); + } + echo "updated.\n"; + } + } + } - if (empty($obsolete)) { - echo "nothing obsoleted...skipped.\n"; - } else { - if ($removeUnused) { - $db->createCommand() - ->delete($sourceMessageTable, ['in', 'id', $obsolete])->execute(); - echo "deleted.\n"; - } else { - $last_id = $db->getLastInsertID(); - $db->createCommand() - ->update( - $sourceMessageTable, - ['message' => new \yii\db\Expression("CONCAT('@@',message,'@@')")], - ['in', 'id', $obsolete] - )->execute(); - foreach ($languages as $language) { - $db->createCommand() - ->insert($messageTable, ['id' => $last_id, 'language' => $language])->execute(); - } - echo "updated.\n"; - } - } - } + /** + * Extracts messages from a file + * + * @param string $fileName name of the file to extract messages from + * @param string $translator name of the function used to translate messages + * @return array + */ + protected function extractMessages($fileName, $translator) + { + echo "Extracting messages from $fileName...\n"; + $subject = file_get_contents($fileName); + $messages = []; + if (!is_array($translator)) { + $translator = [$translator]; + } + foreach ($translator as $currentTranslator) { + $n = preg_match_all( + '/\b' . $currentTranslator . '\s*\(\s*(\'.*?(? 0) { - $merged[$message] = $translated[$message]; - } else { - $untranslated[] = $message; - } - } - ksort($merged); - sort($untranslated); - $todo = []; - foreach ($untranslated as $message) { - $todo[$message] = ''; - } - ksort($translated); - foreach ($translated as $message => $translation) { - if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) { - if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') { - $todo[$message] = $translation; - } else { - $todo[$message] = '@@' . $translation . '@@'; - } - } - } - $merged = array_merge($todo, $merged); - if ($sort) { - ksort($merged); - } - if (false === $overwrite) { - $fileName .= '.merged'; - } - if ($format === 'po') { - $output = ''; - foreach ($merged as $k => $v) { - $k = preg_replace('/(\")|(\\\")/', "\\\"", $k); - $v = preg_replace('/(\")|(\\\")/', "\\\"", $v); - if (substr($v, 0, 2) === '@@' && substr($v, -2) === '@@') { - $output .= "#msgid \"$k\"\n"; - $output .= "#msgstr \"$v\"\n"; - } else { - $output .= "msgid \"$k\"\n"; - $output .= "msgstr \"$v\"\n"; - } - $output .= "\n"; - } - $merged = $output; - } - echo "translation merged.\n"; - } else { - if ($format === 'po') { - $merged = ''; - sort($messages); - foreach ($messages as $message) { - $message = preg_replace('/(\")|(\\\")/', '\\\"', $message); - $merged .= "msgid \"$message\"\n"; - $merged .= "msgstr \"\"\n"; - $merged .= "\n"; - } - } else { - $merged = []; - foreach ($messages as $message) { - $merged[$message] = ''; - } - ksort($merged); - } - echo "saved.\n"; - } - if ($format === 'po') { - $content = $merged; - } else { - $array = str_replace("\r", '', var_export($merged, true)); - $content = << 0) { + $merged[$message] = $translated[$message]; + } else { + $untranslated[] = $message; + } + } + ksort($merged); + sort($untranslated); + $todo = []; + foreach ($untranslated as $message) { + $todo[$message] = ''; + } + ksort($translated); + foreach ($translated as $message => $translation) { + if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) { + if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') { + $todo[$message] = $translation; + } else { + $todo[$message] = '@@' . $translation . '@@'; + } + } + } + $merged = array_merge($todo, $merged); + if ($sort) { + ksort($merged); + } + if (false === $overwrite) { + $fileName .= '.merged'; + } + if ($format === 'po') { + $output = ''; + foreach ($merged as $k => $v) { + $k = preg_replace('/(\")|(\\\")/', "\\\"", $k); + $v = preg_replace('/(\")|(\\\")/', "\\\"", $v); + if (substr($v, 0, 2) === '@@' && substr($v, -2) === '@@') { + $output .= "#msgid \"$k\"\n"; + $output .= "#msgstr \"$v\"\n"; + } else { + $output .= "msgid \"$k\"\n"; + $output .= "msgstr \"$v\"\n"; + } + $output .= "\n"; + } + $merged = $output; + } + echo "translation merged.\n"; + } else { + if ($format === 'po') { + $merged = ''; + sort($messages); + foreach ($messages as $message) { + $message = preg_replace('/(\")|(\\\")/', '\\\"', $message); + $merged .= "msgid \"$message\"\n"; + $merged .= "msgstr \"\"\n"; + $merged .= "\n"; + } + } else { + $merged = []; + foreach ($messages as $message) { + $merged[$message] = ''; + } + ksort($merged); + } + echo "saved.\n"; + } + if ($format === 'po') { + $content = $merged; + } else { + $array = str_replace("\r", '', var_export($merged, true)); + $content = <<migrationPath); - if (!is_dir($path)) { - echo ""; - FileHelper::createDirectory($path); - } - $this->migrationPath = $path; - - if ($action->id !== 'create') { - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); - } - } - - $version = Yii::getVersion(); - echo "Yii Migration Tool (based on Yii v{$version})\n\n"; - return true; - } else { - return false; - } - } - - /** - * Upgrades the application by applying new migrations. - * For example, - * - * ~~~ - * yii migrate # apply all new migrations - * yii migrate 3 # apply the first 3 new migrations - * ~~~ - * - * @param integer $limit the number of new migrations to be applied. If 0, it means - * applying all available new migrations. - */ - public function actionUp($limit = 0) - { - $migrations = $this->getNewMigrations(); - if (empty($migrations)) { - echo "No new migration found. Your system is up-to-date.\n"; - return; - } - - $total = count($migrations); - $limit = (int)$limit; - if ($limit > 0) { - $migrations = array_slice($migrations, 0, $limit); - } - - $n = count($migrations); - if ($n === $total) { - echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } else { - echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } - - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateUp($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - echo "\nMigrated up successfully.\n"; - } - } - - /** - * Downgrades the application by reverting old migrations. - * For example, - * - * ~~~ - * yii migrate/down # revert the last migration - * yii migrate/down 3 # revert the last 3 migrations - * ~~~ - * - * @param integer $limit the number of migrations to be reverted. Defaults to 1, - * meaning the last applied migration will be reverted. - * @throws Exception if the number of the steps specified is less than 1. - */ - public function actionDown($limit = 1) - { - $limit = (int)$limit; - if ($limit < 1) { - throw new Exception("The step argument must be greater than 0."); - } - - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateDown($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - echo "\nMigrated down successfully.\n"; - } - } - - /** - * Redoes the last few migrations. - * - * This command will first revert the specified migrations, and then apply - * them again. For example, - * - * ~~~ - * yii migrate/redo # redo the last applied migration - * yii migrate/redo 3 # redo the last 3 applied migrations - * ~~~ - * - * @param integer $limit the number of migrations to be redone. Defaults to 1, - * meaning the last applied migration will be redone. - * @throws Exception if the number of the steps specified is less than 1. - */ - public function actionRedo($limit = 1) - { - $limit = (int)$limit; - if ($limit < 1) { - throw new Exception("The step argument must be greater than 0."); - } - - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateDown($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - foreach (array_reverse($migrations) as $migration) { - if (!$this->migrateUp($migration)) { - echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; - return; - } - } - echo "\nMigration redone successfully.\n"; - } - } - - /** - * Upgrades or downgrades till the specified version. - * - * Can also downgrade versions to the certain apply time in the past by providing - * a UNIX timestamp or a string parseable by the strtotime() function. This means - * that all the versions applied after the specified certain time would be reverted. - * - * This command will first revert the specified migrations, and then apply - * them again. For example, - * - * ~~~ - * yii migrate/to 101129_185401 # using timestamp - * yii migrate/to m101129_185401_create_user_table # using full name - * yii migrate/to 1392853618 # using UNIX timestamp - * yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string - * ~~~ - * - * @param string $version either the version name or the certain time value in the past - * that the application should be migrated to. This can be either the timestamp, - * the full name of the migration, the UNIX timestamp, or the parseable datetime - * string. - * @throws Exception if the version argument is invalid. - */ - public function actionTo($version) - { - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $this->migrateToVersion('m' . $matches[1]); - } elseif ((string)(int)$version == $version) { - $this->migrateToTime($version); - } elseif (($time = strtotime($version)) !== false) { - $this->migrateToTime($time); - } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50)."); - } - } - - /** - * Modifies the migration history to the specified version. - * - * No actual migration will be performed. - * - * ~~~ - * yii migrate/mark 101129_185401 # using timestamp - * yii migrate/mark m101129_185401_create_user_table # using full name - * ~~~ - * - * @param string $version the version at which the migration history should be marked. - * This can be either the timestamp or the full name of the migration. - * @throws Exception if the version argument is invalid or the version cannot be found. - */ - public function actionMark($version) - { - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); - } - - // try mark up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $this->db->createCommand(); - for ($j = 0; $j <= $i; ++$j) { - $command->insert($this->migrationTable, [ - 'version' => $migrations[$j], - 'apply_time' => time(), - ])->execute(); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - return; - } - } - - // try mark down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $this->db->createCommand(); - for ($j = 0; $j < $i; ++$j) { - $command->delete($this->migrationTable, [ - 'version' => $migrations[$j], - ])->execute(); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - } - return; - } - } - - throw new Exception("Unable to find the version '$originalVersion'."); - } - - /** - * Displays the migration history. - * - * This command will show the list of migrations that have been applied - * so far. For example, - * - * ~~~ - * yii migrate/history # showing the last 10 migrations - * yii migrate/history 5 # showing the last 5 migrations - * yii migrate/history 0 # showing the whole history - * ~~~ - * - * @param integer $limit the maximum number of migrations to be displayed. - * If it is 0, the whole migration history will be displayed. - */ - public function actionHistory($limit = 10) - { - $limit = (int)$limit; - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - echo "No migration has been done before.\n"; - } else { - $n = count($migrations); - if ($limit > 0) { - echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; - } - foreach ($migrations as $version => $time) { - echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; - } - } - } - - /** - * Displays the un-applied new migrations. - * - * This command will show the new migrations that have not been applied. - * For example, - * - * ~~~ - * yii migrate/new # showing the first 10 new migrations - * yii migrate/new 5 # showing the first 5 new migrations - * yii migrate/new 0 # showing all new migrations - * ~~~ - * - * @param integer $limit the maximum number of new migrations to be displayed. - * If it is 0, all available new migrations will be displayed. - */ - public function actionNew($limit = 10) - { - $limit = (int)$limit; - $migrations = $this->getNewMigrations(); - if (empty($migrations)) { - echo "No new migrations found. Your system is up-to-date.\n"; - } else { - $n = count($migrations); - if ($limit > 0 && $n > $limit) { - $migrations = array_slice($migrations, 0, $limit); - echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } - - foreach ($migrations as $migration) { - echo " " . $migration . "\n"; - } - } - } - - /** - * Creates a new migration. - * - * This command creates a new migration using the available migration template. - * After using this command, developers should modify the created migration - * skeleton by filling up the actual migration logic. - * - * ~~~ - * yii migrate/create create_user_table - * ~~~ - * - * @param string $name the name of the new migration. This should only contain - * letters, digits and/or underscores. - * @throws Exception if the name argument is invalid. - */ - public function actionCreate($name) - { - if (!preg_match('/^\w+$/', $name)) { - throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); - } - - $name = 'm' . gmdate('ymd_His') . '_' . $name; - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; - - if ($this->confirm("Create new migration '$file'?")) { - $content = $this->renderFile(Yii::getAlias($this->templateFile), ['className' => $name]); - file_put_contents($file, $content); - echo "New migration created successfully.\n"; - } - } - - /** - * Upgrades with the specified migration class. - * @param string $class the migration class name - * @return boolean whether the migration is successful - */ - protected function migrateUp($class) - { - if ($class === self::BASE_MIGRATION) { - return true; - } - - echo "*** applying $class\n"; - $start = microtime(true); - $migration = $this->createMigration($class); - if ($migration->up() !== false) { - $this->db->createCommand()->insert($this->migrationTable, [ - 'version' => $class, - 'apply_time' => time(), - ])->execute(); - $time = microtime(true) - $start; - echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return true; - } else { - $time = microtime(true) - $start; - echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - /** - * Downgrades with the specified migration class. - * @param string $class the migration class name - * @return boolean whether the migration is successful - */ - protected function migrateDown($class) - { - if ($class === self::BASE_MIGRATION) { - return true; - } - - echo "*** reverting $class\n"; - $start = microtime(true); - $migration = $this->createMigration($class); - if ($migration->down() !== false) { - $this->db->createCommand()->delete($this->migrationTable, [ - 'version' => $class, - ])->execute(); - $time = microtime(true) - $start; - echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return true; - } else { - $time = microtime(true) - $start; - echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - /** - * Creates a new migration instance. - * @param string $class the migration class name - * @return \yii\db\Migration the migration instance - */ - protected function createMigration($class) - { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); - return new $class(['db' => $this->db]); - } - - /** - * Migrates to the specified apply time in the past. - * @param integer $time UNIX timestamp value. - */ - protected function migrateToTime($time) - { - $count = 0; - $migrations = array_values($this->getMigrationHistory(-1)); - while ($count < count($migrations) && $migrations[$count] > $time) { - ++$count; - } - if ($count === 0) { - echo "Nothing needs to be done.\n"; - } else { - $this->actionDown($count); - } - } - - /** - * Migrates to the certain version. - * @param string $version name in the full format. - * @throws Exception if the provided version cannot be found. - */ - protected function migrateToVersion($version) - { - $originalVersion = $version; - - // try migrate up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - $this->actionUp($i + 1); - return; - } - } - - // try migrate down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - $this->actionDown($i); - } - return; - } - } - - throw new Exception("Unable to find the version '$originalVersion'."); - } - - /** - * Returns the migration history. - * @param integer $limit the maximum number of records in the history to be returned - * @return array the migration history - */ - protected function getMigrationHistory($limit) - { - if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) { - $this->createMigrationHistoryTable(); - } - $query = new Query; - $rows = $query->select(['version', 'apply_time']) - ->from($this->migrationTable) - ->orderBy('version DESC') - ->limit($limit) - ->createCommand($this->db) - ->queryAll(); - $history = ArrayHelper::map($rows, 'version', 'apply_time'); - unset($history[self::BASE_MIGRATION]); - return $history; - } - - /** - * Creates the migration history table. - */ - protected function createMigrationHistoryTable() - { - $tableName = $this->db->schema->getRawTableName($this->migrationTable); - echo "Creating migration history table \"$tableName\"..."; - $this->db->createCommand()->createTable($this->migrationTable, [ - 'version' => 'varchar(180) NOT NULL PRIMARY KEY', - 'apply_time' => 'integer', - ])->execute(); - $this->db->createCommand()->insert($this->migrationTable, [ - 'version' => self::BASE_MIGRATION, - 'apply_time' => time(), - ])->execute(); - echo "done.\n"; - } - - /** - * Returns the migrations that are not applied. - * @return array list of new migrations - */ - protected function getNewMigrations() - { - $applied = []; - foreach ($this->getMigrationHistory(-1) as $version => $time) { - $applied[substr($version, 1, 13)] = true; - } - - $migrations = []; - $handle = opendir($this->migrationPath); - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; - if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { - $migrations[] = $matches[1]; - } - } - closedir($handle); - sort($migrations); - return $migrations; - } + /** + * The name of the dummy migration that marks the beginning of the whole migration history. + */ + const BASE_MIGRATION = 'm000000_000000_base'; + + /** + * @var string the default command action. + */ + public $defaultAction = 'up'; + /** + * @var string the directory storing the migration classes. This can be either + * a path alias or a directory. + */ + public $migrationPath = '@app/migrations'; + /** + * @var string the name of the table for keeping applied migration information. + */ + public $migrationTable = '{{%migration}}'; + /** + * @var string the template file for generating new migrations. + * This can be either a path alias (e.g. "@app/migrations/template.php") + * or a file path. + */ + public $templateFile = '@yii/views/migration.php'; + /** + * @var boolean whether to execute the migration in an interactive mode. + */ + public $interactive = true; + /** + * @var Connection|string the DB connection object or the application + * component ID of the DB connection. + */ + public $db = 'db'; + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function options($id) + { + return array_merge(parent::options($id), + ['migrationPath', 'migrationTable', 'db'], // global for all actions + ($id == 'create') ? ['templateFile'] : [] // action create + ); + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * It checks the existence of the [[migrationPath]]. + * @param \yii\base\Action $action the action to be executed. + * @throws Exception if db component isn't configured + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $path = Yii::getAlias($this->migrationPath); + if (!is_dir($path)) { + echo ""; + FileHelper::createDirectory($path); + } + $this->migrationPath = $path; + + if ($action->id !== 'create') { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); + } + } + + $version = Yii::getVersion(); + echo "Yii Migration Tool (based on Yii v{$version})\n\n"; + + return true; + } else { + return false; + } + } + + /** + * Upgrades the application by applying new migrations. + * For example, + * + * ~~~ + * yii migrate # apply all new migrations + * yii migrate 3 # apply the first 3 new migrations + * ~~~ + * + * @param integer $limit the number of new migrations to be applied. If 0, it means + * applying all available new migrations. + */ + public function actionUp($limit = 0) + { + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { + echo "No new migration found. Your system is up-to-date.\n"; + + return; + } + + $total = count($migrations); + $limit = (int) $limit; + if ($limit > 0) { + $migrations = array_slice($migrations, 0, $limit); + } + + $n = count($migrations); + if ($n === $total) { + echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } else { + echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } + + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + + return; + } + } + echo "\nMigrated up successfully.\n"; + } + } + + /** + * Downgrades the application by reverting old migrations. + * For example, + * + * ~~~ + * yii migrate/down # revert the last migration + * yii migrate/down 3 # revert the last 3 migrations + * ~~~ + * + * @param integer $limit the number of migrations to be reverted. Defaults to 1, + * meaning the last applied migration will be reverted. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionDown($limit = 1) + { + $limit = (int) $limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + echo "No migration has been done before.\n"; + + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + + return; + } + } + echo "\nMigrated down successfully.\n"; + } + } + + /** + * Redoes the last few migrations. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yii migrate/redo # redo the last applied migration + * yii migrate/redo 3 # redo the last 3 applied migrations + * ~~~ + * + * @param integer $limit the number of migrations to be redone. Defaults to 1, + * meaning the last applied migration will be redone. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionRedo($limit = 1) + { + $limit = (int) $limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + echo "No migration has been done before.\n"; + + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + + return; + } + } + foreach (array_reverse($migrations) as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; + + return; + } + } + echo "\nMigration redone successfully.\n"; + } + } + + /** + * Upgrades or downgrades till the specified version. + * + * Can also downgrade versions to the certain apply time in the past by providing + * a UNIX timestamp or a string parseable by the strtotime() function. This means + * that all the versions applied after the specified certain time would be reverted. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yii migrate/to 101129_185401 # using timestamp + * yii migrate/to m101129_185401_create_user_table # using full name + * yii migrate/to 1392853618 # using UNIX timestamp + * yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string + * ~~~ + * + * @param string $version either the version name or the certain time value in the past + * that the application should be migrated to. This can be either the timestamp, + * the full name of the migration, the UNIX timestamp, or the parseable datetime + * string. + * @throws Exception if the version argument is invalid. + */ + public function actionTo($version) + { + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $this->migrateToVersion('m' . $matches[1]); + } elseif ((string) (int) $version == $version) { + $this->migrateToTime($version); + } elseif (($time = strtotime($version)) !== false) { + $this->migrateToTime($time); + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50)."); + } + } + + /** + * Modifies the migration history to the specified version. + * + * No actual migration will be performed. + * + * ~~~ + * yii migrate/mark 101129_185401 # using timestamp + * yii migrate/mark m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version at which the migration history should be marked. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid or the version cannot be found. + */ + public function actionMark($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try mark up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j <= $i; ++$j) { + $command->insert($this->migrationTable, [ + 'version' => $migrations[$j], + 'apply_time' => time(), + ])->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + + return; + } + } + + // try mark down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j < $i; ++$j) { + $command->delete($this->migrationTable, [ + 'version' => $migrations[$j], + ])->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + } + + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Displays the migration history. + * + * This command will show the list of migrations that have been applied + * so far. For example, + * + * ~~~ + * yii migrate/history # showing the last 10 migrations + * yii migrate/history 5 # showing the last 5 migrations + * yii migrate/history 0 # showing the whole history + * ~~~ + * + * @param integer $limit the maximum number of migrations to be displayed. + * If it is 0, the whole migration history will be displayed. + */ + public function actionHistory($limit = 10) + { + $limit = (int) $limit; + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + echo "No migration has been done before.\n"; + } else { + $n = count($migrations); + if ($limit > 0) { + echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; + } + foreach ($migrations as $version => $time) { + echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; + } + } + } + + /** + * Displays the un-applied new migrations. + * + * This command will show the new migrations that have not been applied. + * For example, + * + * ~~~ + * yii migrate/new # showing the first 10 new migrations + * yii migrate/new 5 # showing the first 5 new migrations + * yii migrate/new 0 # showing all new migrations + * ~~~ + * + * @param integer $limit the maximum number of new migrations to be displayed. + * If it is 0, all available new migrations will be displayed. + */ + public function actionNew($limit = 10) + { + $limit = (int) $limit; + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { + echo "No new migrations found. Your system is up-to-date.\n"; + } else { + $n = count($migrations); + if ($limit > 0 && $n > $limit) { + $migrations = array_slice($migrations, 0, $limit); + echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } + + foreach ($migrations as $migration) { + echo " " . $migration . "\n"; + } + } + } + + /** + * Creates a new migration. + * + * This command creates a new migration using the available migration template. + * After using this command, developers should modify the created migration + * skeleton by filling up the actual migration logic. + * + * ~~~ + * yii migrate/create create_user_table + * ~~~ + * + * @param string $name the name of the new migration. This should only contain + * letters, digits and/or underscores. + * @throws Exception if the name argument is invalid. + */ + public function actionCreate($name) + { + if (!preg_match('/^\w+$/', $name)) { + throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); + } + + $name = 'm' . gmdate('ymd_His') . '_' . $name; + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; + + if ($this->confirm("Create new migration '$file'?")) { + $content = $this->renderFile(Yii::getAlias($this->templateFile), ['className' => $name]); + file_put_contents($file, $content); + echo "New migration created successfully.\n"; + } + } + + /** + * Upgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateUp($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** applying $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->up() !== false) { + $this->db->createCommand()->insert($this->migrationTable, [ + 'version' => $class, + 'apply_time' => time(), + ])->execute(); + $time = microtime(true) - $start; + echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + + return false; + } + } + + /** + * Downgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateDown($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** reverting $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->down() !== false) { + $this->db->createCommand()->delete($this->migrationTable, [ + 'version' => $class, + ])->execute(); + $time = microtime(true) - $start; + echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + + return false; + } + } + + /** + * Creates a new migration instance. + * @param string $class the migration class name + * @return \yii\db\Migration the migration instance + */ + protected function createMigration($class) + { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + + return new $class(['db' => $this->db]); + } + + /** + * Migrates to the specified apply time in the past. + * @param integer $time UNIX timestamp value. + */ + protected function migrateToTime($time) + { + $count = 0; + $migrations = array_values($this->getMigrationHistory(-1)); + while ($count < count($migrations) && $migrations[$count] > $time) { + ++$count; + } + if ($count === 0) { + echo "Nothing needs to be done.\n"; + } else { + $this->actionDown($count); + } + } + + /** + * Migrates to the certain version. + * @param string $version name in the full format. + * @throws Exception if the provided version cannot be found. + */ + protected function migrateToVersion($version) + { + $originalVersion = $version; + + // try migrate up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + $this->actionUp($i + 1); + + return; + } + } + + // try migrate down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + $this->actionDown($i); + } + + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Returns the migration history. + * @param integer $limit the maximum number of records in the history to be returned + * @return array the migration history + */ + protected function getMigrationHistory($limit) + { + if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) { + $this->createMigrationHistoryTable(); + } + $query = new Query; + $rows = $query->select(['version', 'apply_time']) + ->from($this->migrationTable) + ->orderBy('version DESC') + ->limit($limit) + ->createCommand($this->db) + ->queryAll(); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + + return $history; + } + + /** + * Creates the migration history table. + */ + protected function createMigrationHistoryTable() + { + $tableName = $this->db->schema->getRawTableName($this->migrationTable); + echo "Creating migration history table \"$tableName\"..."; + $this->db->createCommand()->createTable($this->migrationTable, [ + 'version' => 'varchar(180) NOT NULL PRIMARY KEY', + 'apply_time' => 'integer', + ])->execute(); + $this->db->createCommand()->insert($this->migrationTable, [ + 'version' => self::BASE_MIGRATION, + 'apply_time' => time(), + ])->execute(); + echo "done.\n"; + } + + /** + * Returns the migrations that are not applied. + * @return array list of new migrations + */ + protected function getNewMigrations() + { + $applied = []; + foreach ($this->getMigrationHistory(-1) as $version => $time) { + $applied[substr($version, 1, 13)] = true; + } + + $migrations = []; + $handle = opendir($this->migrationPath); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { + $migrations[] = $matches[1]; + } + } + closedir($handle); + sort($migrations); + + return $migrations; + } } diff --git a/framework/data/ActiveDataProvider.php b/framework/data/ActiveDataProvider.php index 6618153f8ce..073eb1b500c 100644 --- a/framework/data/ActiveDataProvider.php +++ b/framework/data/ActiveDataProvider.php @@ -53,130 +53,134 @@ */ class ActiveDataProvider extends BaseDataProvider { - /** - * @var QueryInterface the query that is used to fetch data models and [[totalCount]] - * if it is not explicitly set. - */ - public $query; - /** - * @var string|callable the column that is used as the key of the data models. - * This can be either a column name, or a callable that returns the key value of a given data model. - * - * If this is not set, the following rules will be used to determine the keys of the data models: - * - * - If [[query]] is an [[\yii\db\ActiveQuery]] instance, the primary keys of [[\yii\db\ActiveQuery::modelClass]] will be used. - * - Otherwise, the keys of the [[models]] array will be used. - * - * @see getKeys() - */ - public $key; - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * If not set, the default DB connection will be used. - */ - public $db; + /** + * @var QueryInterface the query that is used to fetch data models and [[totalCount]] + * if it is not explicitly set. + */ + public $query; + /** + * @var string|callable the column that is used as the key of the data models. + * This can be either a column name, or a callable that returns the key value of a given data model. + * + * If this is not set, the following rules will be used to determine the keys of the data models: + * + * - If [[query]] is an [[\yii\db\ActiveQuery]] instance, the primary keys of [[\yii\db\ActiveQuery::modelClass]] will be used. + * - Otherwise, the keys of the [[models]] array will be used. + * + * @see getKeys() + */ + public $key; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * If not set, the default DB connection will be used. + */ + public $db; - /** - * Initializes the DB connection component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - if ($this->db === null) { - throw new InvalidConfigException('The "db" property must be a valid DB Connection application component.'); - } - } - } + /** + * Initializes the DB connection component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + if ($this->db === null) { + throw new InvalidConfigException('The "db" property must be a valid DB Connection application component.'); + } + } + } - /** - * @inheritdoc - */ - protected function prepareModels() - { - if (!$this->query instanceof QueryInterface) { - throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); - } - if (($pagination = $this->getPagination()) !== false) { - $pagination->totalCount = $this->getTotalCount(); - $this->query->limit($pagination->getLimit())->offset($pagination->getOffset()); - } - if (($sort = $this->getSort()) !== false) { - $this->query->addOrderBy($sort->getOrders()); - } - return $this->query->all($this->db); - } + /** + * @inheritdoc + */ + protected function prepareModels() + { + if (!$this->query instanceof QueryInterface) { + throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); + } + if (($pagination = $this->getPagination()) !== false) { + $pagination->totalCount = $this->getTotalCount(); + $this->query->limit($pagination->getLimit())->offset($pagination->getOffset()); + } + if (($sort = $this->getSort()) !== false) { + $this->query->addOrderBy($sort->getOrders()); + } - /** - * @inheritdoc - */ - protected function prepareKeys($models) - { - $keys = []; - if ($this->key !== null) { - foreach ($models as $model) { - if (is_string($this->key)) { - $keys[] = $model[$this->key]; - } else { - $keys[] = call_user_func($this->key, $model); - } - } - return $keys; - } elseif ($this->query instanceof ActiveQueryInterface) { - /** @var \yii\db\ActiveRecord $class */ - $class = $this->query->modelClass; - $pks = $class::primaryKey(); - if (count($pks) === 1) { - $pk = $pks[0]; - foreach ($models as $model) { - $keys[] = $model[$pk]; - } - } else { - foreach ($models as $model) { - $kk = []; - foreach ($pks as $pk) { - $kk[$pk] = $model[$pk]; - } - $keys[] = $kk; - } - } - return $keys; - } else { - return array_keys($models); - } - } + return $this->query->all($this->db); + } - /** - * @inheritdoc - */ - protected function prepareTotalCount() - { - if (!$this->query instanceof QueryInterface) { - throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); - } - $query = clone $this->query; - return (int) $query->limit(-1)->offset(-1)->orderBy([])->count('*', $this->db); - } + /** + * @inheritdoc + */ + protected function prepareKeys($models) + { + $keys = []; + if ($this->key !== null) { + foreach ($models as $model) { + if (is_string($this->key)) { + $keys[] = $model[$this->key]; + } else { + $keys[] = call_user_func($this->key, $model); + } + } - /** - * @inheritdoc - */ - public function setSort($value) - { - parent::setSort($value); - if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQueryInterface) { - /** @var Model $model */ - $model = new $this->query->modelClass; - foreach ($model->attributes() as $attribute) { - $sort->attributes[$attribute] = [ - 'asc' => [$attribute => SORT_ASC], - 'desc' => [$attribute => SORT_DESC], - 'label' => $model->getAttributeLabel($attribute), - ]; - } - } - } + return $keys; + } elseif ($this->query instanceof ActiveQueryInterface) { + /** @var \yii\db\ActiveRecord $class */ + $class = $this->query->modelClass; + $pks = $class::primaryKey(); + if (count($pks) === 1) { + $pk = $pks[0]; + foreach ($models as $model) { + $keys[] = $model[$pk]; + } + } else { + foreach ($models as $model) { + $kk = []; + foreach ($pks as $pk) { + $kk[$pk] = $model[$pk]; + } + $keys[] = $kk; + } + } + + return $keys; + } else { + return array_keys($models); + } + } + + /** + * @inheritdoc + */ + protected function prepareTotalCount() + { + if (!$this->query instanceof QueryInterface) { + throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); + } + $query = clone $this->query; + + return (int) $query->limit(-1)->offset(-1)->orderBy([])->count('*', $this->db); + } + + /** + * @inheritdoc + */ + public function setSort($value) + { + parent::setSort($value); + if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQueryInterface) { + /** @var Model $model */ + $model = new $this->query->modelClass; + foreach ($model->attributes() as $attribute) { + $sort->attributes[$attribute] = [ + 'asc' => [$attribute => SORT_ASC], + 'desc' => [$attribute => SORT_DESC], + 'label' => $model->getAttributeLabel($attribute), + ]; + } + } + } } diff --git a/framework/data/ArrayDataProvider.php b/framework/data/ArrayDataProvider.php index 2b694c7c060..4ec2d117046 100644 --- a/framework/data/ArrayDataProvider.php +++ b/framework/data/ArrayDataProvider.php @@ -50,82 +50,83 @@ */ class ArrayDataProvider extends BaseDataProvider { - /** - * @var string|callable the column that is used as the key of the data models. - * This can be either a column name, or a callable that returns the key value of a given data model. - * If this is not set, the index of the [[models]] array will be used. - * @see getKeys() - */ - public $key; - /** - * @var array the data that is not paginated or sorted. When pagination is enabled, - * this property usually contains more elements than [[models]]. - * The array elements must use zero-based integer keys. - */ - public $allModels; + /** + * @var string|callable the column that is used as the key of the data models. + * This can be either a column name, or a callable that returns the key value of a given data model. + * If this is not set, the index of the [[models]] array will be used. + * @see getKeys() + */ + public $key; + /** + * @var array the data that is not paginated or sorted. When pagination is enabled, + * this property usually contains more elements than [[models]]. + * The array elements must use zero-based integer keys. + */ + public $allModels; + /** + * @inheritdoc + */ + protected function prepareModels() + { + if (($models = $this->allModels) === null) { + return []; + } - /** - * @inheritdoc - */ - protected function prepareModels() - { - if (($models = $this->allModels) === null) { - return []; - } + if (($sort = $this->getSort()) !== false) { + $models = $this->sortModels($models, $sort); + } - if (($sort = $this->getSort()) !== false) { - $models = $this->sortModels($models, $sort); - } + if (($pagination = $this->getPagination()) !== false) { + $pagination->totalCount = $this->getTotalCount(); + $models = array_slice($models, $pagination->getOffset(), $pagination->getLimit()); + } - if (($pagination = $this->getPagination()) !== false) { - $pagination->totalCount = $this->getTotalCount(); - $models = array_slice($models, $pagination->getOffset(), $pagination->getLimit()); - } + return $models; + } - return $models; - } + /** + * @inheritdoc + */ + protected function prepareKeys($models) + { + if ($this->key !== null) { + $keys = []; + foreach ($models as $model) { + if (is_string($this->key)) { + $keys[] = $model[$this->key]; + } else { + $keys[] = call_user_func($this->key, $model); + } + } - /** - * @inheritdoc - */ - protected function prepareKeys($models) - { - if ($this->key !== null) { - $keys = []; - foreach ($models as $model) { - if (is_string($this->key)) { - $keys[] = $model[$this->key]; - } else { - $keys[] = call_user_func($this->key, $model); - } - } - return $keys; - } else { - return array_keys($models); - } - } + return $keys; + } else { + return array_keys($models); + } + } - /** - * @inheritdoc - */ - protected function prepareTotalCount() - { - return count($this->allModels); - } + /** + * @inheritdoc + */ + protected function prepareTotalCount() + { + return count($this->allModels); + } - /** - * Sorts the data models according to the given sort definition - * @param array $models the models to be sorted - * @param Sort $sort the sort definition - * @return array the sorted data models - */ - protected function sortModels($models, $sort) - { - $orders = $sort->getOrders(); - if (!empty($orders)) { - ArrayHelper::multisort($models, array_keys($orders), array_values($orders)); - } - return $models; - } + /** + * Sorts the data models according to the given sort definition + * @param array $models the models to be sorted + * @param Sort $sort the sort definition + * @return array the sorted data models + */ + protected function sortModels($models, $sort) + { + $orders = $sort->getOrders(); + if (!empty($orders)) { + ArrayHelper::multisort($models, array_keys($orders), array_values($orders)); + } + + return $models; + } } diff --git a/framework/data/BaseDataProvider.php b/framework/data/BaseDataProvider.php index 31acc2d263c..6c9c395322e 100644 --- a/framework/data/BaseDataProvider.php +++ b/framework/data/BaseDataProvider.php @@ -30,221 +30,225 @@ */ abstract class BaseDataProvider extends Component implements DataProviderInterface { - /** - * @var string an ID that uniquely identifies the data provider among all data providers. - * You should set this property if the same page contains two or more different data providers. - * Otherwise, the [[pagination]] and [[sort]] mainly not work properly. - */ - public $id; - - private $_sort; - private $_pagination; - private $_keys; - private $_models; - private $_totalCount; - - - /** - * Prepares the data models that will be made available in the current page. - * @return array the available data models - */ - abstract protected function prepareModels(); - - /** - * Prepares the keys associated with the currently available data models. - * @param array $models the available data models - * @return array the keys - */ - abstract protected function prepareKeys($models); - - /** - * Returns a value indicating the total number of data models in this data provider. - * @return integer total number of data models in this data provider. - */ - abstract protected function prepareTotalCount(); - - /** - * Prepares the data models and keys. - * - * This method will prepare the data models and keys that can be retrieved via - * [[getModels()]] and [[getKeys()]]. - * - * This method will be implicitly called by [[getModels()]] and [[getKeys()]] if it has not been called before. - * - * @param boolean $forcePrepare whether to force data preparation even if it has been done before. - */ - public function prepare($forcePrepare = false) - { - if ($forcePrepare || $this->_models === null) { - $this->_models = $this->prepareModels(); - } - if ($forcePrepare || $this->_keys === null) { - $this->_keys = $this->prepareKeys($this->_models); - } - } - - /** - * Returns the data models in the current page. - * @return array the list of data models in the current page. - */ - public function getModels() - { - $this->prepare(); - return $this->_models; - } - - /** - * Sets the data models in the current page. - * @param array $models the models in the current page - */ - public function setModels($models) - { - $this->_models = $models; - } - - /** - * Returns the key values associated with the data models. - * @return array the list of key values corresponding to [[models]]. Each data model in [[models]] - * is uniquely identified by the corresponding key value in this array. - */ - public function getKeys() - { - $this->prepare(); - return $this->_keys; - } - - /** - * Sets the key values associated with the data models. - * @param array $keys the list of key values corresponding to [[models]]. - */ - public function setKeys($keys) - { - $this->_keys = $keys; - } - - /** - * Returns the number of data models in the current page. - * @return integer the number of data models in the current page. - */ - public function getCount() - { - return count($this->getModels()); - } - - /** - * Returns the total number of data models. - * When [[pagination]] is false, this returns the same value as [[count]]. - * Otherwise, it will call [[prepareTotalCount()]] to get the count. - * @return integer total number of possible data models. - */ - public function getTotalCount() - { - if ($this->getPagination() === false) { - return $this->getCount(); - } elseif ($this->_totalCount === null) { - $this->_totalCount = $this->prepareTotalCount(); - } - return $this->_totalCount; - } - - /** - * Sets the total number of data models. - * @param integer $value the total number of data models. - */ - public function setTotalCount($value) - { - $this->_totalCount = $value; - } - - /** - * Returns the pagination object used by this data provider. - * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values - * of [[Pagination::totalCount]] and [[Pagination::pageCount]]. - * @return Pagination|boolean the pagination object. If this is false, it means the pagination is disabled. - */ - public function getPagination() - { - if ($this->_pagination === null) { - $this->setPagination([]); - } - return $this->_pagination; - } - - /** - * Sets the pagination for this data provider. - * @param array|Pagination|boolean $value the pagination to be used by this data provider. - * This can be one of the following: - * - * - a configuration array for creating the pagination object. The "class" element defaults - * to 'yii\data\Pagination' - * - an instance of [[Pagination]] or its subclass - * - false, if pagination needs to be disabled. - * - * @throws InvalidParamException - */ - public function setPagination($value) - { - if (is_array($value)) { - $config = ['class' => Pagination::className()]; - if ($this->id !== null) { - $config['pageParam'] = $this->id . '-page'; - $config['pageSizeParam'] = $this->id . '-per-page'; - } - $this->_pagination = Yii::createObject(array_merge($config, $value)); - } elseif ($value instanceof Pagination || $value === false) { - $this->_pagination = $value; - } else { - throw new InvalidParamException('Only Pagination instance, configuration array or false is allowed.'); - } - } - - /** - * @return Sort|boolean the sorting object. If this is false, it means the sorting is disabled. - */ - public function getSort() - { - if ($this->_sort === null) { - $this->setSort([]); - } - return $this->_sort; - } - - /** - * Sets the sort definition for this data provider. - * @param array|Sort|boolean $value the sort definition to be used by this data provider. - * This can be one of the following: - * - * - a configuration array for creating the sort definition object. The "class" element defaults - * to 'yii\data\Sort' - * - an instance of [[Sort]] or its subclass - * - false, if sorting needs to be disabled. - * - * @throws InvalidParamException - */ - public function setSort($value) - { - if (is_array($value)) { - $config = ['class' => Sort::className()]; - if ($this->id !== null) { - $config['sortParam'] = $this->id . '-sort'; - } - $this->_sort = Yii::createObject(array_merge($config, $value)); - } elseif ($value instanceof Sort || $value === false) { - $this->_sort = $value; - } else { - throw new InvalidParamException('Only Sort instance, configuration array or false is allowed.'); - } - } - - /** - * Refreshes the data provider. - * After calling this method, if [[getModels()]], [[getKeys()]] or [[getTotalCount()]] is called again, - * they will re-execute the query and return the latest data available. - */ - public function refresh() - { - $this->_totalCount = null; - $this->_models = null; - $this->_keys = null; - } + /** + * @var string an ID that uniquely identifies the data provider among all data providers. + * You should set this property if the same page contains two or more different data providers. + * Otherwise, the [[pagination]] and [[sort]] mainly not work properly. + */ + public $id; + + private $_sort; + private $_pagination; + private $_keys; + private $_models; + private $_totalCount; + + /** + * Prepares the data models that will be made available in the current page. + * @return array the available data models + */ + abstract protected function prepareModels(); + + /** + * Prepares the keys associated with the currently available data models. + * @param array $models the available data models + * @return array the keys + */ + abstract protected function prepareKeys($models); + + /** + * Returns a value indicating the total number of data models in this data provider. + * @return integer total number of data models in this data provider. + */ + abstract protected function prepareTotalCount(); + + /** + * Prepares the data models and keys. + * + * This method will prepare the data models and keys that can be retrieved via + * [[getModels()]] and [[getKeys()]]. + * + * This method will be implicitly called by [[getModels()]] and [[getKeys()]] if it has not been called before. + * + * @param boolean $forcePrepare whether to force data preparation even if it has been done before. + */ + public function prepare($forcePrepare = false) + { + if ($forcePrepare || $this->_models === null) { + $this->_models = $this->prepareModels(); + } + if ($forcePrepare || $this->_keys === null) { + $this->_keys = $this->prepareKeys($this->_models); + } + } + + /** + * Returns the data models in the current page. + * @return array the list of data models in the current page. + */ + public function getModels() + { + $this->prepare(); + + return $this->_models; + } + + /** + * Sets the data models in the current page. + * @param array $models the models in the current page + */ + public function setModels($models) + { + $this->_models = $models; + } + + /** + * Returns the key values associated with the data models. + * @return array the list of key values corresponding to [[models]]. Each data model in [[models]] + * is uniquely identified by the corresponding key value in this array. + */ + public function getKeys() + { + $this->prepare(); + + return $this->_keys; + } + + /** + * Sets the key values associated with the data models. + * @param array $keys the list of key values corresponding to [[models]]. + */ + public function setKeys($keys) + { + $this->_keys = $keys; + } + + /** + * Returns the number of data models in the current page. + * @return integer the number of data models in the current page. + */ + public function getCount() + { + return count($this->getModels()); + } + + /** + * Returns the total number of data models. + * When [[pagination]] is false, this returns the same value as [[count]]. + * Otherwise, it will call [[prepareTotalCount()]] to get the count. + * @return integer total number of possible data models. + */ + public function getTotalCount() + { + if ($this->getPagination() === false) { + return $this->getCount(); + } elseif ($this->_totalCount === null) { + $this->_totalCount = $this->prepareTotalCount(); + } + + return $this->_totalCount; + } + + /** + * Sets the total number of data models. + * @param integer $value the total number of data models. + */ + public function setTotalCount($value) + { + $this->_totalCount = $value; + } + + /** + * Returns the pagination object used by this data provider. + * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values + * of [[Pagination::totalCount]] and [[Pagination::pageCount]]. + * @return Pagination|boolean the pagination object. If this is false, it means the pagination is disabled. + */ + public function getPagination() + { + if ($this->_pagination === null) { + $this->setPagination([]); + } + + return $this->_pagination; + } + + /** + * Sets the pagination for this data provider. + * @param array|Pagination|boolean $value the pagination to be used by this data provider. + * This can be one of the following: + * + * - a configuration array for creating the pagination object. The "class" element defaults + * to 'yii\data\Pagination' + * - an instance of [[Pagination]] or its subclass + * - false, if pagination needs to be disabled. + * + * @throws InvalidParamException + */ + public function setPagination($value) + { + if (is_array($value)) { + $config = ['class' => Pagination::className()]; + if ($this->id !== null) { + $config['pageParam'] = $this->id . '-page'; + $config['pageSizeParam'] = $this->id . '-per-page'; + } + $this->_pagination = Yii::createObject(array_merge($config, $value)); + } elseif ($value instanceof Pagination || $value === false) { + $this->_pagination = $value; + } else { + throw new InvalidParamException('Only Pagination instance, configuration array or false is allowed.'); + } + } + + /** + * @return Sort|boolean the sorting object. If this is false, it means the sorting is disabled. + */ + public function getSort() + { + if ($this->_sort === null) { + $this->setSort([]); + } + + return $this->_sort; + } + + /** + * Sets the sort definition for this data provider. + * @param array|Sort|boolean $value the sort definition to be used by this data provider. + * This can be one of the following: + * + * - a configuration array for creating the sort definition object. The "class" element defaults + * to 'yii\data\Sort' + * - an instance of [[Sort]] or its subclass + * - false, if sorting needs to be disabled. + * + * @throws InvalidParamException + */ + public function setSort($value) + { + if (is_array($value)) { + $config = ['class' => Sort::className()]; + if ($this->id !== null) { + $config['sortParam'] = $this->id . '-sort'; + } + $this->_sort = Yii::createObject(array_merge($config, $value)); + } elseif ($value instanceof Sort || $value === false) { + $this->_sort = $value; + } else { + throw new InvalidParamException('Only Sort instance, configuration array or false is allowed.'); + } + } + + /** + * Refreshes the data provider. + * After calling this method, if [[getModels()]], [[getKeys()]] or [[getTotalCount()]] is called again, + * they will re-execute the query and return the latest data available. + */ + public function refresh() + { + $this->_totalCount = null; + $this->_models = null; + $this->_keys = null; + } } diff --git a/framework/data/DataProviderInterface.php b/framework/data/DataProviderInterface.php index cce3b2dab2b..400bba0509b 100644 --- a/framework/data/DataProviderInterface.php +++ b/framework/data/DataProviderInterface.php @@ -18,53 +18,53 @@ */ interface DataProviderInterface { - /** - * Prepares the data models and keys. - * - * This method will prepare the data models and keys that can be retrieved via - * [[getModels()]] and [[getKeys()]]. - * - * This method will be implicitly called by [[getModels()]] and [[getKeys()]] if it has not been called before. - * - * @param boolean $forcePrepare whether to force data preparation even if it has been done before. - */ - public function prepare($forcePrepare = false); + /** + * Prepares the data models and keys. + * + * This method will prepare the data models and keys that can be retrieved via + * [[getModels()]] and [[getKeys()]]. + * + * This method will be implicitly called by [[getModels()]] and [[getKeys()]] if it has not been called before. + * + * @param boolean $forcePrepare whether to force data preparation even if it has been done before. + */ + public function prepare($forcePrepare = false); - /** - * Returns the number of data models in the current page. - * This is equivalent to `count($provider->getModels())`. - * When [[getPagination|pagination]] is false, this is the same as [[getTotalCount|totalCount]]. - * @return integer the number of data models in the current page. - */ - public function getCount(); + /** + * Returns the number of data models in the current page. + * This is equivalent to `count($provider->getModels())`. + * When [[getPagination|pagination]] is false, this is the same as [[getTotalCount|totalCount]]. + * @return integer the number of data models in the current page. + */ + public function getCount(); - /** - * Returns the total number of data models. - * When [[getPagination|pagination]] is false, this is the same as [[getCount|count]]. - * @return integer total number of possible data models. - */ - public function getTotalCount(); + /** + * Returns the total number of data models. + * When [[getPagination|pagination]] is false, this is the same as [[getCount|count]]. + * @return integer total number of possible data models. + */ + public function getTotalCount(); - /** - * Returns the data models in the current page. - * @return array the list of data models in the current page. - */ - public function getModels(); + /** + * Returns the data models in the current page. + * @return array the list of data models in the current page. + */ + public function getModels(); - /** - * Returns the key values associated with the data models. - * @return array the list of key values corresponding to [[getModels|models]]. Each data model in [[getModels|models]] - * is uniquely identified by the corresponding key value in this array. - */ - public function getKeys(); + /** + * Returns the key values associated with the data models. + * @return array the list of key values corresponding to [[getModels|models]]. Each data model in [[getModels|models]] + * is uniquely identified by the corresponding key value in this array. + */ + public function getKeys(); - /** - * @return Sort the sorting object. If this is false, it means the sorting is disabled. - */ - public function getSort(); + /** + * @return Sort the sorting object. If this is false, it means the sorting is disabled. + */ + public function getSort(); - /** - * @return Pagination the pagination object. If this is false, it means the pagination is disabled. - */ - public function getPagination(); + /** + * @return Pagination the pagination object. If this is false, it means the pagination is disabled. + */ + public function getPagination(); } diff --git a/framework/data/Pagination.php b/framework/data/Pagination.php index a5ef86ea208..5bbb5159c45 100644 --- a/framework/data/Pagination.php +++ b/framework/data/Pagination.php @@ -72,265 +72,271 @@ */ class Pagination extends Object implements Linkable { - const LINK_NEXT = 'next'; - const LINK_PREV = 'prev'; - const LINK_FIRST = 'first'; - const LINK_LAST = 'last'; + const LINK_NEXT = 'next'; + const LINK_PREV = 'prev'; + const LINK_FIRST = 'first'; + const LINK_LAST = 'last'; - /** - * @var string name of the parameter storing the current page index. - * @see params - */ - public $pageParam = 'page'; - /** - * @var string name of the parameter storing the page size. - * @see params - */ - public $pageSizeParam = 'per-page'; - /** - * @var boolean whether to always have the page parameter in the URL created by [[createUrl()]]. - * If false and [[page]] is 0, the page parameter will not be put in the URL. - */ - public $forcePageParam = true; - /** - * @var string the route of the controller action for displaying the paged contents. - * If not set, it means using the currently requested route. - */ - public $route; - /** - * @var array parameters (name => value) that should be used to obtain the current page number - * and to create new pagination URLs. If not set, all parameters from $_GET will be used instead. - * - * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`. - * - * The array element indexed by [[pageParam]] is considered to be the current page number (defaults to 0); - * while the element indexed by [[pageSizeParam]] is treated as the page size (defaults to [[defaultPageSize]]). - */ - public $params; - /** - * @var \yii\web\UrlManager the URL manager used for creating pagination URLs. If not set, - * the "urlManager" application component will be used. - */ - public $urlManager; - /** - * @var boolean whether to check if [[page]] is within valid range. - * When this property is true, the value of [[page]] will always be between 0 and ([[pageCount]]-1). - * Because [[pageCount]] relies on the correct value of [[totalCount]] which may not be available - * in some cases (e.g. MongoDB), you may want to set this property to be false to disable the page - * number validation. By doing so, [[page]] will return the value indexed by [[pageParam]] in [[params]]. - */ - public $validatePage = true; - /** - * @var integer total number of items. - */ - public $totalCount = 0; - /** - * @var integer the default page size. This property will be returned by [[pageSize]] when page size - * cannot be determined by [[pageSizeParam]] from [[params]]. - */ - public $defaultPageSize = 20; - /** - * @var array|boolean the page size limits. The first array element stands for the minimal page size, and the second - * the maximal page size. If this is false, it means [[pageSize]] should always return the value of [[defaultPageSize]]. - */ - public $pageSizeLimit = [1, 50]; - /** - * @var integer number of items on each page. - * If it is less than 1, it means the page size is infinite, and thus a single page contains all items. - */ - private $_pageSize; + /** + * @var string name of the parameter storing the current page index. + * @see params + */ + public $pageParam = 'page'; + /** + * @var string name of the parameter storing the page size. + * @see params + */ + public $pageSizeParam = 'per-page'; + /** + * @var boolean whether to always have the page parameter in the URL created by [[createUrl()]]. + * If false and [[page]] is 0, the page parameter will not be put in the URL. + */ + public $forcePageParam = true; + /** + * @var string the route of the controller action for displaying the paged contents. + * If not set, it means using the currently requested route. + */ + public $route; + /** + * @var array parameters (name => value) that should be used to obtain the current page number + * and to create new pagination URLs. If not set, all parameters from $_GET will be used instead. + * + * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`. + * + * The array element indexed by [[pageParam]] is considered to be the current page number (defaults to 0); + * while the element indexed by [[pageSizeParam]] is treated as the page size (defaults to [[defaultPageSize]]). + */ + public $params; + /** + * @var \yii\web\UrlManager the URL manager used for creating pagination URLs. If not set, + * the "urlManager" application component will be used. + */ + public $urlManager; + /** + * @var boolean whether to check if [[page]] is within valid range. + * When this property is true, the value of [[page]] will always be between 0 and ([[pageCount]]-1). + * Because [[pageCount]] relies on the correct value of [[totalCount]] which may not be available + * in some cases (e.g. MongoDB), you may want to set this property to be false to disable the page + * number validation. By doing so, [[page]] will return the value indexed by [[pageParam]] in [[params]]. + */ + public $validatePage = true; + /** + * @var integer total number of items. + */ + public $totalCount = 0; + /** + * @var integer the default page size. This property will be returned by [[pageSize]] when page size + * cannot be determined by [[pageSizeParam]] from [[params]]. + */ + public $defaultPageSize = 20; + /** + * @var array|boolean the page size limits. The first array element stands for the minimal page size, and the second + * the maximal page size. If this is false, it means [[pageSize]] should always return the value of [[defaultPageSize]]. + */ + public $pageSizeLimit = [1, 50]; + /** + * @var integer number of items on each page. + * If it is less than 1, it means the page size is infinite, and thus a single page contains all items. + */ + private $_pageSize; + /** + * @return integer number of pages + */ + public function getPageCount() + { + $pageSize = $this->getPageSize(); + if ($pageSize < 1) { + return $this->totalCount > 0 ? 1 : 0; + } else { + $totalCount = $this->totalCount < 0 ? 0 : (int) $this->totalCount; - /** - * @return integer number of pages - */ - public function getPageCount() - { - $pageSize = $this->getPageSize(); - if ($pageSize < 1) { - return $this->totalCount > 0 ? 1 : 0; - } else { - $totalCount = $this->totalCount < 0 ? 0 : (int)$this->totalCount; - return (int)(($totalCount + $pageSize - 1) / $pageSize); - } - } + return (int) (($totalCount + $pageSize - 1) / $pageSize); + } + } - private $_page; + private $_page; - /** - * Returns the zero-based current page number. - * @param boolean $recalculate whether to recalculate the current page based on the page size and item count. - * @return integer the zero-based current page number. - */ - public function getPage($recalculate = false) - { - if ($this->_page === null || $recalculate) { - $page = (int)$this->getQueryParam($this->pageParam, 1) - 1; - $this->setPage($page, true); - } - return $this->_page; - } + /** + * Returns the zero-based current page number. + * @param boolean $recalculate whether to recalculate the current page based on the page size and item count. + * @return integer the zero-based current page number. + */ + public function getPage($recalculate = false) + { + if ($this->_page === null || $recalculate) { + $page = (int) $this->getQueryParam($this->pageParam, 1) - 1; + $this->setPage($page, true); + } - /** - * Sets the current page number. - * @param integer $value the zero-based index of the current page. - * @param boolean $validatePage whether to validate the page number. Note that in order - * to validate the page number, both [[validatePage]] and this parameter must be true. - */ - public function setPage($value, $validatePage = false) - { - if ($value === null) { - $this->_page = null; - } else { - $value = (int)$value; - if ($validatePage && $this->validatePage) { - $pageCount = $this->getPageCount(); - if ($value >= $pageCount) { - $value = $pageCount - 1; - } - } - if ($value < 0) { - $value = 0; - } - $this->_page = $value; - } - } + return $this->_page; + } - /** - * Returns the number of items per page. - * By default, this method will try to determine the page size by [[pageSizeParam]] in [[params]]. - * If the page size cannot be determined this way, [[defaultPageSize]] will be returned. - * @return integer the number of items per page. - * @see pageSizeLimit - */ - public function getPageSize() - { - if ($this->_pageSize === null) { - if (empty($this->pageSizeLimit)) { - $pageSize = $this->defaultPageSize; - $this->setPageSize($pageSize); - } else { - $pageSize = (int)$this->getQueryParam($this->pageSizeParam, $this->defaultPageSize); - $this->setPageSize($pageSize, true); - } - } - return $this->_pageSize; - } + /** + * Sets the current page number. + * @param integer $value the zero-based index of the current page. + * @param boolean $validatePage whether to validate the page number. Note that in order + * to validate the page number, both [[validatePage]] and this parameter must be true. + */ + public function setPage($value, $validatePage = false) + { + if ($value === null) { + $this->_page = null; + } else { + $value = (int) $value; + if ($validatePage && $this->validatePage) { + $pageCount = $this->getPageCount(); + if ($value >= $pageCount) { + $value = $pageCount - 1; + } + } + if ($value < 0) { + $value = 0; + } + $this->_page = $value; + } + } - /** - * @param integer $value the number of items per page. - * @param boolean $validatePageSize whether to validate page size. - */ - public function setPageSize($value, $validatePageSize = false) - { - if ($value === null) { - $this->_pageSize = null; - } else { - $value = (int)$value; - if ($validatePageSize && count($this->pageSizeLimit) === 2 && isset($this->pageSizeLimit[0], $this->pageSizeLimit[1])) { - if ($value < $this->pageSizeLimit[0]) { - $value = $this->pageSizeLimit[0]; - } elseif ($value > $this->pageSizeLimit[1]) { - $value = $this->pageSizeLimit[1]; - } - } - $this->_pageSize = $value; - } - } + /** + * Returns the number of items per page. + * By default, this method will try to determine the page size by [[pageSizeParam]] in [[params]]. + * If the page size cannot be determined this way, [[defaultPageSize]] will be returned. + * @return integer the number of items per page. + * @see pageSizeLimit + */ + public function getPageSize() + { + if ($this->_pageSize === null) { + if (empty($this->pageSizeLimit)) { + $pageSize = $this->defaultPageSize; + $this->setPageSize($pageSize); + } else { + $pageSize = (int) $this->getQueryParam($this->pageSizeParam, $this->defaultPageSize); + $this->setPageSize($pageSize, true); + } + } - /** - * Creates the URL suitable for pagination with the specified page number. - * This method is mainly called by pagers when creating URLs used to perform pagination. - * @param integer $page the zero-based page number that the URL should point to. - * @param boolean $absolute whether to create an absolute URL. Defaults to `false`. - * @return string the created URL - * @see params - * @see forcePageParam - */ - public function createUrl($page, $absolute = false) - { - if (($params = $this->params) === null) { - $request = Yii::$app->getRequest(); - $params = $request instanceof Request ? $request->getQueryParams() : []; - } - if ($page > 0 || $page >= 0 && $this->forcePageParam) { - $params[$this->pageParam] = $page + 1; - } else { - unset($params[$this->pageParam]); - } - $pageSize = $this->getPageSize(); - if ($pageSize != $this->defaultPageSize) { - $params[$this->pageSizeParam] = $pageSize; - } else { - unset($params[$this->pageSizeParam]); - } - $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; - $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; - if ($absolute) { - return $urlManager->createAbsoluteUrl($params); - } else { - return $urlManager->createUrl($params); - } - } + return $this->_pageSize; + } - /** - * @return integer the offset of the data. This may be used to set the - * OFFSET value for a SQL statement for fetching the current page of data. - */ - public function getOffset() - { - $pageSize = $this->getPageSize(); - return $pageSize < 1 ? 0 : $this->getPage() * $pageSize; - } + /** + * @param integer $value the number of items per page. + * @param boolean $validatePageSize whether to validate page size. + */ + public function setPageSize($value, $validatePageSize = false) + { + if ($value === null) { + $this->_pageSize = null; + } else { + $value = (int) $value; + if ($validatePageSize && count($this->pageSizeLimit) === 2 && isset($this->pageSizeLimit[0], $this->pageSizeLimit[1])) { + if ($value < $this->pageSizeLimit[0]) { + $value = $this->pageSizeLimit[0]; + } elseif ($value > $this->pageSizeLimit[1]) { + $value = $this->pageSizeLimit[1]; + } + } + $this->_pageSize = $value; + } + } - /** - * @return integer the limit of the data. This may be used to set the - * LIMIT value for a SQL statement for fetching the current page of data. - * Note that if the page size is infinite, a value -1 will be returned. - */ - public function getLimit() - { - $pageSize = $this->getPageSize(); - return $pageSize < 1 ? -1 : $pageSize; - } + /** + * Creates the URL suitable for pagination with the specified page number. + * This method is mainly called by pagers when creating URLs used to perform pagination. + * @param integer $page the zero-based page number that the URL should point to. + * @param boolean $absolute whether to create an absolute URL. Defaults to `false`. + * @return string the created URL + * @see params + * @see forcePageParam + */ + public function createUrl($page, $absolute = false) + { + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->getQueryParams() : []; + } + if ($page > 0 || $page >= 0 && $this->forcePageParam) { + $params[$this->pageParam] = $page + 1; + } else { + unset($params[$this->pageParam]); + } + $pageSize = $this->getPageSize(); + if ($pageSize != $this->defaultPageSize) { + $params[$this->pageSizeParam] = $pageSize; + } else { + unset($params[$this->pageSizeParam]); + } + $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; + $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; + if ($absolute) { + return $urlManager->createAbsoluteUrl($params); + } else { + return $urlManager->createUrl($params); + } + } - /** - * Returns a whole set of links for navigating to the first, last, next and previous pages. - * @param boolean $absolute whether the generated URLs should be absolute. - * @return array the links for navigational purpose. The array keys specify the purpose of the links (e.g. [[LINK_FIRST]]), - * and the array values are the corresponding URLs. - */ - public function getLinks($absolute = false) - { - $currentPage = $this->getPage(); - $pageCount = $this->getPageCount(); - $links = [ - Link::REL_SELF => $this->createUrl($currentPage, $absolute), - ]; - if ($currentPage > 0) { - $links[self::LINK_FIRST] = $this->createUrl(0, $absolute); - $links[self::LINK_PREV] = $this->createUrl($currentPage - 1, $absolute); - } - if ($currentPage < $pageCount - 1) { - $links[self::LINK_NEXT] = $this->createUrl($currentPage + 1, $absolute); - $links[self::LINK_LAST] = $this->createUrl($pageCount - 1, $absolute); - } - return $links; - } + /** + * @return integer the offset of the data. This may be used to set the + * OFFSET value for a SQL statement for fetching the current page of data. + */ + public function getOffset() + { + $pageSize = $this->getPageSize(); - /** - * Returns the value of the specified query parameter. - * This method returns the named parameter value from [[params]]. Null is returned if the value does not exist. - * @param string $name the parameter name - * @param string $defaultValue the value to be returned when the specified parameter does not exist in [[params]]. - * @return string the parameter value - */ - protected function getQueryParam($name, $defaultValue = null) - { - if (($params = $this->params) === null) { - $request = Yii::$app->getRequest(); - $params = $request instanceof Request ? $request->getQueryParams() : []; - } - return isset($params[$name]) && is_scalar($params[$name]) ? $params[$name] : $defaultValue; - } + return $pageSize < 1 ? 0 : $this->getPage() * $pageSize; + } + + /** + * @return integer the limit of the data. This may be used to set the + * LIMIT value for a SQL statement for fetching the current page of data. + * Note that if the page size is infinite, a value -1 will be returned. + */ + public function getLimit() + { + $pageSize = $this->getPageSize(); + + return $pageSize < 1 ? -1 : $pageSize; + } + + /** + * Returns a whole set of links for navigating to the first, last, next and previous pages. + * @param boolean $absolute whether the generated URLs should be absolute. + * @return array the links for navigational purpose. The array keys specify the purpose of the links (e.g. [[LINK_FIRST]]), + * and the array values are the corresponding URLs. + */ + public function getLinks($absolute = false) + { + $currentPage = $this->getPage(); + $pageCount = $this->getPageCount(); + $links = [ + Link::REL_SELF => $this->createUrl($currentPage, $absolute), + ]; + if ($currentPage > 0) { + $links[self::LINK_FIRST] = $this->createUrl(0, $absolute); + $links[self::LINK_PREV] = $this->createUrl($currentPage - 1, $absolute); + } + if ($currentPage < $pageCount - 1) { + $links[self::LINK_NEXT] = $this->createUrl($currentPage + 1, $absolute); + $links[self::LINK_LAST] = $this->createUrl($pageCount - 1, $absolute); + } + + return $links; + } + + /** + * Returns the value of the specified query parameter. + * This method returns the named parameter value from [[params]]. Null is returned if the value does not exist. + * @param string $name the parameter name + * @param string $defaultValue the value to be returned when the specified parameter does not exist in [[params]]. + * @return string the parameter value + */ + protected function getQueryParam($name, $defaultValue = null) + { + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->getQueryParams() : []; + } + + return isset($params[$name]) && is_scalar($params[$name]) ? $params[$name] : $defaultValue; + } } diff --git a/framework/data/Sort.php b/framework/data/Sort.php index cfcbb82a392..dec5dedb981 100644 --- a/framework/data/Sort.php +++ b/framework/data/Sort.php @@ -76,316 +76,321 @@ */ class Sort extends Object { - /** - * @var boolean whether the sorting can be applied to multiple attributes simultaneously. - * Defaults to false, which means each time the data can only be sorted by one attribute. - */ - public $enableMultiSort = false; + /** + * @var boolean whether the sorting can be applied to multiple attributes simultaneously. + * Defaults to false, which means each time the data can only be sorted by one attribute. + */ + public $enableMultiSort = false; - /** - * @var array list of attributes that are allowed to be sorted. Its syntax can be - * described using the following example: - * - * ~~~ - * [ - * 'age', - * 'name' => [ - * 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - * 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - * 'default' => SORT_DESC, - * 'label' => 'Name', - * ], - * ] - * ~~~ - * - * In the above, two attributes are declared: "age" and "name". The "age" attribute is - * a simple attribute which is equivalent to the following: - * - * ~~~ - * 'age' => [ - * 'asc' => ['age' => SORT_ASC], - * 'desc' => ['age' => SORT_DESC], - * 'default' => SORT_ASC, - * 'label' => Inflector::camel2words('age'), - * ] - * ~~~ - * - * The "name" attribute is a composite attribute: - * - * - The "name" key represents the attribute name which will appear in the URLs leading - * to sort actions. - * - The "asc" and "desc" elements specify how to sort by the attribute in ascending - * and descending orders, respectively. Their values represent the actual columns and - * the directions by which the data should be sorted by. - * - The "default" element specifies by which direction the attribute should be sorted - * if it is not currently sorted (the default value is ascending order). - * - The "label" element specifies what label should be used when calling [[link()]] to create - * a sort link. If not set, [[Inflector::camel2words()]] will be called to get a label. - * Note that it will not be HTML-encoded. - * - * Note that if the Sort object is already created, you can only use the full format - * to configure every attribute. Each attribute must include these elements: `asc` and `desc`. - */ - public $attributes = []; - /** - * @var string the name of the parameter that specifies which attributes to be sorted - * in which direction. Defaults to 'sort'. - * @see params - */ - public $sortParam = 'sort'; - /** - * @var array the order that should be used when the current request does not specify any order. - * The array keys are attribute names and the array values are the corresponding sort directions. For example, - * - * ~~~ - * [ - * 'name' => SORT_ASC, - * 'created_at' => SORT_DESC, - * ] - * ~~~ - * - * @see attributeOrders - */ - public $defaultOrder; - /** - * @var string the route of the controller action for displaying the sorted contents. - * If not set, it means using the currently requested route. - */ - public $route; - /** - * @var string the character used to separate different attributes that need to be sorted by. - */ - public $separator = ','; - /** - * @var array parameters (name => value) that should be used to obtain the current sort directions - * and to create new sort URLs. If not set, $_GET will be used instead. - * - * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`. - * - * The array element indexed by [[sortParam]] is considered to be the current sort directions. - * If the element does not exist, the [[defaultOrder|default order]] will be used. - * - * @see sortParam - * @see defaultOrder - */ - public $params; - /** - * @var \yii\web\UrlManager the URL manager used for creating sort URLs. If not set, - * the "urlManager" application component will be used. - */ - public $urlManager; + /** + * @var array list of attributes that are allowed to be sorted. Its syntax can be + * described using the following example: + * + * ~~~ + * [ + * 'age', + * 'name' => [ + * 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + * 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + * 'default' => SORT_DESC, + * 'label' => 'Name', + * ], + * ] + * ~~~ + * + * In the above, two attributes are declared: "age" and "name". The "age" attribute is + * a simple attribute which is equivalent to the following: + * + * ~~~ + * 'age' => [ + * 'asc' => ['age' => SORT_ASC], + * 'desc' => ['age' => SORT_DESC], + * 'default' => SORT_ASC, + * 'label' => Inflector::camel2words('age'), + * ] + * ~~~ + * + * The "name" attribute is a composite attribute: + * + * - The "name" key represents the attribute name which will appear in the URLs leading + * to sort actions. + * - The "asc" and "desc" elements specify how to sort by the attribute in ascending + * and descending orders, respectively. Their values represent the actual columns and + * the directions by which the data should be sorted by. + * - The "default" element specifies by which direction the attribute should be sorted + * if it is not currently sorted (the default value is ascending order). + * - The "label" element specifies what label should be used when calling [[link()]] to create + * a sort link. If not set, [[Inflector::camel2words()]] will be called to get a label. + * Note that it will not be HTML-encoded. + * + * Note that if the Sort object is already created, you can only use the full format + * to configure every attribute. Each attribute must include these elements: `asc` and `desc`. + */ + public $attributes = []; + /** + * @var string the name of the parameter that specifies which attributes to be sorted + * in which direction. Defaults to 'sort'. + * @see params + */ + public $sortParam = 'sort'; + /** + * @var array the order that should be used when the current request does not specify any order. + * The array keys are attribute names and the array values are the corresponding sort directions. For example, + * + * ~~~ + * [ + * 'name' => SORT_ASC, + * 'created_at' => SORT_DESC, + * ] + * ~~~ + * + * @see attributeOrders + */ + public $defaultOrder; + /** + * @var string the route of the controller action for displaying the sorted contents. + * If not set, it means using the currently requested route. + */ + public $route; + /** + * @var string the character used to separate different attributes that need to be sorted by. + */ + public $separator = ','; + /** + * @var array parameters (name => value) that should be used to obtain the current sort directions + * and to create new sort URLs. If not set, $_GET will be used instead. + * + * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`. + * + * The array element indexed by [[sortParam]] is considered to be the current sort directions. + * If the element does not exist, the [[defaultOrder|default order]] will be used. + * + * @see sortParam + * @see defaultOrder + */ + public $params; + /** + * @var \yii\web\UrlManager the URL manager used for creating sort URLs. If not set, + * the "urlManager" application component will be used. + */ + public $urlManager; - /** - * Normalizes the [[attributes]] property. - */ - public function init() - { - $attributes = []; - foreach ($this->attributes as $name => $attribute) { - if (!is_array($attribute)) { - $attributes[$attribute] = [ - 'asc' => [$attribute => SORT_ASC], - 'desc' => [$attribute => SORT_DESC], - ]; - } elseif (!isset($attribute['asc'], $attribute['desc'])) { - $attributes[$name] = array_merge([ - 'asc' => [$name => SORT_ASC], - 'desc' => [$name => SORT_DESC], - ], $attribute); - } else { - $attributes[$name] = $attribute; - } - } - $this->attributes = $attributes; - } + /** + * Normalizes the [[attributes]] property. + */ + public function init() + { + $attributes = []; + foreach ($this->attributes as $name => $attribute) { + if (!is_array($attribute)) { + $attributes[$attribute] = [ + 'asc' => [$attribute => SORT_ASC], + 'desc' => [$attribute => SORT_DESC], + ]; + } elseif (!isset($attribute['asc'], $attribute['desc'])) { + $attributes[$name] = array_merge([ + 'asc' => [$name => SORT_ASC], + 'desc' => [$name => SORT_DESC], + ], $attribute); + } else { + $attributes[$name] = $attribute; + } + } + $this->attributes = $attributes; + } - /** - * Returns the columns and their corresponding sort directions. - * @param boolean $recalculate whether to recalculate the sort directions - * @return array the columns (keys) and their corresponding sort directions (values). - * This can be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. - */ - public function getOrders($recalculate = false) - { - $attributeOrders = $this->getAttributeOrders($recalculate); - $orders = []; - foreach ($attributeOrders as $attribute => $direction) { - $definition = $this->attributes[$attribute]; - $columns = $definition[$direction === SORT_ASC ? 'asc' : 'desc']; - foreach ($columns as $name => $dir) { - $orders[$name] = $dir; - } - } - return $orders; - } + /** + * Returns the columns and their corresponding sort directions. + * @param boolean $recalculate whether to recalculate the sort directions + * @return array the columns (keys) and their corresponding sort directions (values). + * This can be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. + */ + public function getOrders($recalculate = false) + { + $attributeOrders = $this->getAttributeOrders($recalculate); + $orders = []; + foreach ($attributeOrders as $attribute => $direction) { + $definition = $this->attributes[$attribute]; + $columns = $definition[$direction === SORT_ASC ? 'asc' : 'desc']; + foreach ($columns as $name => $dir) { + $orders[$name] = $dir; + } + } - /** - * @var array the currently requested sort order as computed by [[getAttributeOrders]]. - */ - private $_attributeOrders; + return $orders; + } - /** - * Returns the currently requested sort information. - * @param boolean $recalculate whether to recalculate the sort directions - * @return array sort directions indexed by attribute names. - * Sort direction can be either `SORT_ASC` for ascending order or - * `SORT_DESC` for descending order. - */ - public function getAttributeOrders($recalculate = false) - { - if ($this->_attributeOrders === null || $recalculate) { - $this->_attributeOrders = []; - if (($params = $this->params) === null) { - $request = Yii::$app->getRequest(); - $params = $request instanceof Request ? $request->getQueryParams() : []; - } - if (isset($params[$this->sortParam]) && is_scalar($params[$this->sortParam])) { - $attributes = explode($this->separator, $params[$this->sortParam]); - foreach ($attributes as $attribute) { - $descending = false; - if (strncmp($attribute, '-', 1) === 0) { - $descending = true; - $attribute = substr($attribute, 1); - } + /** + * @var array the currently requested sort order as computed by [[getAttributeOrders]]. + */ + private $_attributeOrders; - if (isset($this->attributes[$attribute])) { - $this->_attributeOrders[$attribute] = $descending ? SORT_DESC : SORT_ASC; - if (!$this->enableMultiSort) { - return $this->_attributeOrders; - } - } - } - } - if (empty($this->_attributeOrders) && is_array($this->defaultOrder)) { - $this->_attributeOrders = $this->defaultOrder; - } - } - return $this->_attributeOrders; - } + /** + * Returns the currently requested sort information. + * @param boolean $recalculate whether to recalculate the sort directions + * @return array sort directions indexed by attribute names. + * Sort direction can be either `SORT_ASC` for ascending order or + * `SORT_DESC` for descending order. + */ + public function getAttributeOrders($recalculate = false) + { + if ($this->_attributeOrders === null || $recalculate) { + $this->_attributeOrders = []; + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->getQueryParams() : []; + } + if (isset($params[$this->sortParam]) && is_scalar($params[$this->sortParam])) { + $attributes = explode($this->separator, $params[$this->sortParam]); + foreach ($attributes as $attribute) { + $descending = false; + if (strncmp($attribute, '-', 1) === 0) { + $descending = true; + $attribute = substr($attribute, 1); + } - /** - * Returns the sort direction of the specified attribute in the current request. - * @param string $attribute the attribute name - * @return boolean|null Sort direction of the attribute. Can be either `SORT_ASC` - * for ascending order or `SORT_DESC` for descending order. Null is returned - * if the attribute is invalid or does not need to be sorted. - */ - public function getAttributeOrder($attribute) - { - $orders = $this->getAttributeOrders(); - return isset($orders[$attribute]) ? $orders[$attribute] : null; - } + if (isset($this->attributes[$attribute])) { + $this->_attributeOrders[$attribute] = $descending ? SORT_DESC : SORT_ASC; + if (!$this->enableMultiSort) { + return $this->_attributeOrders; + } + } + } + } + if (empty($this->_attributeOrders) && is_array($this->defaultOrder)) { + $this->_attributeOrders = $this->defaultOrder; + } + } - /** - * Generates a hyperlink that links to the sort action to sort by the specified attribute. - * Based on the sort direction, the CSS class of the generated hyperlink will be appended - * with "asc" or "desc". - * @param string $attribute the attribute name by which the data should be sorted by. - * @param array $options additional HTML attributes for the hyperlink tag. - * There is one special attribute `label` which will be used as the label of the hyperlink. - * If this is not set, the label defined in [[attributes]] will be used. - * If no label is defined, [[\yii\helpers\Inflector::camel2words()]] will be called to get a label. - * Note that it will not be HTML-encoded. - * @return string the generated hyperlink - * @throws InvalidConfigException if the attribute is unknown - */ - public function link($attribute, $options = []) - { - if (($direction = $this->getAttributeOrder($attribute)) !== null) { - $class = $direction === SORT_DESC ? 'desc' : 'asc'; - if (isset($options['class'])) { - $options['class'] .= ' ' . $class; - } else { - $options['class'] = $class; - } - } + return $this->_attributeOrders; + } - $url = $this->createUrl($attribute); - $options['data-sort'] = $this->createSortParam($attribute); + /** + * Returns the sort direction of the specified attribute in the current request. + * @param string $attribute the attribute name + * @return boolean|null Sort direction of the attribute. Can be either `SORT_ASC` + * for ascending order or `SORT_DESC` for descending order. Null is returned + * if the attribute is invalid or does not need to be sorted. + */ + public function getAttributeOrder($attribute) + { + $orders = $this->getAttributeOrders(); - if (isset($options['label'])) { - $label = $options['label']; - unset($options['label']); - } else { - if (isset($this->attributes[$attribute]['label'])) { - $label = $this->attributes[$attribute]['label']; - } else { - $label = Inflector::camel2words($attribute); - } - } - return Html::a($label, $url, $options); - } + return isset($orders[$attribute]) ? $orders[$attribute] : null; + } - /** - * Creates a URL for sorting the data by the specified attribute. - * This method will consider the current sorting status given by [[attributeOrders]]. - * For example, if the current page already sorts the data by the specified attribute in ascending order, - * then the URL created will lead to a page that sorts the data by the specified attribute in descending order. - * @param string $attribute the attribute name - * @param boolean $absolute whether to create an absolute URL. Defaults to `false`. - * @return string the URL for sorting. False if the attribute is invalid. - * @throws InvalidConfigException if the attribute is unknown - * @see attributeOrders - * @see params - */ - public function createUrl($attribute, $absolute = false) - { - if (($params = $this->params) === null) { - $request = Yii::$app->getRequest(); - $params = $request instanceof Request ? $request->getQueryParams() : []; - } - $params[$this->sortParam] = $this->createSortParam($attribute); - $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; - $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; - if ($absolute) { - return $urlManager->createAbsoluteUrl($params); - } else { - return $urlManager->createUrl($params); - } - } + /** + * Generates a hyperlink that links to the sort action to sort by the specified attribute. + * Based on the sort direction, the CSS class of the generated hyperlink will be appended + * with "asc" or "desc". + * @param string $attribute the attribute name by which the data should be sorted by. + * @param array $options additional HTML attributes for the hyperlink tag. + * There is one special attribute `label` which will be used as the label of the hyperlink. + * If this is not set, the label defined in [[attributes]] will be used. + * If no label is defined, [[\yii\helpers\Inflector::camel2words()]] will be called to get a label. + * Note that it will not be HTML-encoded. + * @return string the generated hyperlink + * @throws InvalidConfigException if the attribute is unknown + */ + public function link($attribute, $options = []) + { + if (($direction = $this->getAttributeOrder($attribute)) !== null) { + $class = $direction === SORT_DESC ? 'desc' : 'asc'; + if (isset($options['class'])) { + $options['class'] .= ' ' . $class; + } else { + $options['class'] = $class; + } + } - /** - * Creates the sort variable for the specified attribute. - * The newly created sort variable can be used to create a URL that will lead to - * sorting by the specified attribute. - * @param string $attribute the attribute name - * @return string the value of the sort variable - * @throws InvalidConfigException if the specified attribute is not defined in [[attributes]] - */ - public function createSortParam($attribute) - { - if (!isset($this->attributes[$attribute])) { - throw new InvalidConfigException("Unknown attribute: $attribute"); - } - $definition = $this->attributes[$attribute]; - $directions = $this->getAttributeOrders(); - if (isset($directions[$attribute])) { - $direction = $directions[$attribute] === SORT_DESC ? SORT_ASC : SORT_DESC; - unset($directions[$attribute]); - } else { - $direction = isset($definition['default']) ? $definition['default'] : SORT_ASC; - } + $url = $this->createUrl($attribute); + $options['data-sort'] = $this->createSortParam($attribute); - if ($this->enableMultiSort) { - $directions = array_merge([$attribute => $direction], $directions); - } else { - $directions = [$attribute => $direction]; - } + if (isset($options['label'])) { + $label = $options['label']; + unset($options['label']); + } else { + if (isset($this->attributes[$attribute]['label'])) { + $label = $this->attributes[$attribute]['label']; + } else { + $label = Inflector::camel2words($attribute); + } + } - $sorts = []; - foreach ($directions as $attribute => $direction) { - $sorts[] = $direction === SORT_DESC ? '-' . $attribute : $attribute; - } - return implode($this->separator, $sorts); - } + return Html::a($label, $url, $options); + } - /** - * Returns a value indicating whether the sort definition supports sorting by the named attribute. - * @param string $name the attribute name - * @return boolean whether the sort definition supports sorting by the named attribute. - */ - public function hasAttribute($name) - { - return isset($this->attributes[$name]); - } + /** + * Creates a URL for sorting the data by the specified attribute. + * This method will consider the current sorting status given by [[attributeOrders]]. + * For example, if the current page already sorts the data by the specified attribute in ascending order, + * then the URL created will lead to a page that sorts the data by the specified attribute in descending order. + * @param string $attribute the attribute name + * @param boolean $absolute whether to create an absolute URL. Defaults to `false`. + * @return string the URL for sorting. False if the attribute is invalid. + * @throws InvalidConfigException if the attribute is unknown + * @see attributeOrders + * @see params + */ + public function createUrl($attribute, $absolute = false) + { + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->getQueryParams() : []; + } + $params[$this->sortParam] = $this->createSortParam($attribute); + $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; + $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; + if ($absolute) { + return $urlManager->createAbsoluteUrl($params); + } else { + return $urlManager->createUrl($params); + } + } + + /** + * Creates the sort variable for the specified attribute. + * The newly created sort variable can be used to create a URL that will lead to + * sorting by the specified attribute. + * @param string $attribute the attribute name + * @return string the value of the sort variable + * @throws InvalidConfigException if the specified attribute is not defined in [[attributes]] + */ + public function createSortParam($attribute) + { + if (!isset($this->attributes[$attribute])) { + throw new InvalidConfigException("Unknown attribute: $attribute"); + } + $definition = $this->attributes[$attribute]; + $directions = $this->getAttributeOrders(); + if (isset($directions[$attribute])) { + $direction = $directions[$attribute] === SORT_DESC ? SORT_ASC : SORT_DESC; + unset($directions[$attribute]); + } else { + $direction = isset($definition['default']) ? $definition['default'] : SORT_ASC; + } + + if ($this->enableMultiSort) { + $directions = array_merge([$attribute => $direction], $directions); + } else { + $directions = [$attribute => $direction]; + } + + $sorts = []; + foreach ($directions as $attribute => $direction) { + $sorts[] = $direction === SORT_DESC ? '-' . $attribute : $attribute; + } + + return implode($this->separator, $sorts); + } + + /** + * Returns a value indicating whether the sort definition supports sorting by the named attribute. + * @param string $name the attribute name + * @return boolean whether the sort definition supports sorting by the named attribute. + */ + public function hasAttribute($name) + { + return isset($this->attributes[$name]); + } } diff --git a/framework/data/SqlDataProvider.php b/framework/data/SqlDataProvider.php index e00ebe20427..090c469fa3f 100644 --- a/framework/data/SqlDataProvider.php +++ b/framework/data/SqlDataProvider.php @@ -61,98 +61,98 @@ */ class SqlDataProvider extends BaseDataProvider { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - */ - public $db = 'db'; - /** - * @var string the SQL statement to be used for fetching data rows. - */ - public $sql; - /** - * @var array parameters (name=>value) to be bound to the SQL statement. - */ - public $params = []; - /** - * @var string|callable the column that is used as the key of the data models. - * This can be either a column name, or a callable that returns the key value of a given data model. - * - * If this is not set, the keys of the [[models]] array will be used. - */ - public $key; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + */ + public $db = 'db'; + /** + * @var string the SQL statement to be used for fetching data rows. + */ + public $sql; + /** + * @var array parameters (name=>value) to be bound to the SQL statement. + */ + public $params = []; + /** + * @var string|callable the column that is used as the key of the data models. + * This can be either a column name, or a callable that returns the key value of a given data model. + * + * If this is not set, the keys of the [[models]] array will be used. + */ + public $key; + /** + * Initializes the DB connection component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException('The "db" property must be a valid DB Connection application component.'); + } + if ($this->sql === null) { + throw new InvalidConfigException('The "sql" property must be set.'); + } + } - /** - * Initializes the DB connection component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException('The "db" property must be a valid DB Connection application component.'); - } - if ($this->sql === null) { - throw new InvalidConfigException('The "sql" property must be set.'); - } - } + /** + * @inheritdoc + */ + protected function prepareModels() + { + $sql = $this->sql; + $qb = $this->db->getQueryBuilder(); + if (($sort = $this->getSort()) !== false) { + $orderBy = $qb->buildOrderBy($sort->getOrders()); + if (!empty($orderBy)) { + $orderBy = substr($orderBy, 9); + if (preg_match('/\s+order\s+by\s+[\w\s,\.]+$/i', $sql)) { + $sql .= ', ' . $orderBy; + } else { + $sql .= ' ORDER BY ' . $orderBy; + } + } + } - /** - * @inheritdoc - */ - protected function prepareModels() - { - $sql = $this->sql; - $qb = $this->db->getQueryBuilder(); - if (($sort = $this->getSort()) !== false) { - $orderBy = $qb->buildOrderBy($sort->getOrders()); - if (!empty($orderBy)) { - $orderBy = substr($orderBy, 9); - if (preg_match('/\s+order\s+by\s+[\w\s,\.]+$/i', $sql)) { - $sql .= ', ' . $orderBy; - } else { - $sql .= ' ORDER BY ' . $orderBy; - } - } - } + if (($pagination = $this->getPagination()) !== false) { + $pagination->totalCount = $this->getTotalCount(); + $sql .= ' ' . $qb->buildLimit($pagination->getLimit(), $pagination->getOffset()); + } - if (($pagination = $this->getPagination()) !== false) { - $pagination->totalCount = $this->getTotalCount(); - $sql .= ' ' . $qb->buildLimit($pagination->getLimit(), $pagination->getOffset()); - } + return $this->db->createCommand($sql, $this->params)->queryAll(); + } - return $this->db->createCommand($sql, $this->params)->queryAll(); - } + /** + * @inheritdoc + */ + protected function prepareKeys($models) + { + $keys = []; + if ($this->key !== null) { + foreach ($models as $model) { + if (is_string($this->key)) { + $keys[] = $model[$this->key]; + } else { + $keys[] = call_user_func($this->key, $model); + } + } - /** - * @inheritdoc - */ - protected function prepareKeys($models) - { - $keys = []; - if ($this->key !== null) { - foreach ($models as $model) { - if (is_string($this->key)) { - $keys[] = $model[$this->key]; - } else { - $keys[] = call_user_func($this->key, $model); - } - } - return $keys; - } else { - return array_keys($models); - } - } + return $keys; + } else { + return array_keys($models); + } + } - /** - * @inheritdoc - */ - protected function prepareTotalCount() - { - return 0; - } + /** + * @inheritdoc + */ + protected function prepareTotalCount() + { + return 0; + } } diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index 55a9a802495..3c53e6d192e 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -70,544 +70,551 @@ */ class ActiveQuery extends Query implements ActiveQueryInterface { - use ActiveQueryTrait; - use ActiveRelationTrait; - - /** - * @var string the SQL statement to be executed for retrieving AR records. - * This is set by [[ActiveRecord::findBySql()]]. - */ - public $sql; - /** - * @var string|array the join condition to be used when this query is used in a relational context. - * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called. - * Otherwise, the condition will be used in the WHERE part of a query. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @see onCondition() - */ - public $on; - /** - * @var array a list of relations that this query should be joined with - */ - public $joinWith; - - - /** - * Executes query and returns all results as an array. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - return parent::all($db); - } - - /** - * @inheritdoc - */ - public function prepareResult($rows) - { - if (empty($rows)) { - return []; - } - - $models = $this->createModels($rows); - if (!empty($this->join) && $this->indexBy === null) { - $models = $this->removeDuplicatedModels($models); - } - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - return $models; - } - - /** - * Removes duplicated models by checking their primary key values. - * This method is mainly called when a join query is performed, which may cause duplicated rows being returned. - * @param array $models the models to be checked - * @return array the distinctive models - */ - private function removeDuplicatedModels($models) - { - $hash = []; - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $pks = $class::primaryKey(); - - if (count($pks) > 1) { - foreach ($models as $i => $model) { - $key = []; - foreach ($pks as $pk) { - $key[] = $model[$pk]; - } - $key = serialize($key); - if (isset($hash[$key])) { - unset($models[$i]); - } else { - $hash[$key] = true; - } - } - } else { - $pk = reset($pks); - foreach ($models as $i => $model) { - $key = $model[$pk]; - if (isset($hash[$key])) { - unset($models[$i]); - } else { - $hash[$key] = true; - } - } - } - - return array_values($models); - } - - /** - * Executes query and returns a single row of result. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - $command = $this->createCommand($db); - $row = $command->queryOne(); - if ($row !== false) { - if ($this->asArray) { - $model = $row; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - if (!$this->asArray) { - $model->afterFind(); - } - return $model; - } else { - return null; - } - } - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($this->primaryModel === null) { - // not a relational context or eager loading - if (!empty($this->on)) { - $where = $this->where; - $this->andWhere($this->on); - $command = $this->createCommandInternal($db); - $this->where = $where; - return $command; - } else { - return $this->createCommandInternal($db); - } - } else { - // lazy loading of a relation - return $this->createRelationalCommand($db); - } - } - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return Command the created DB command instance. - */ - protected function createCommandInternal($db) - { - /** @var ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } - - if ($this->sql === null) { - if (!empty($this->joinWith)) { - $this->buildJoinWith(); - $this->joinWith = null; // clean it up to avoid issue https://github.com/yiisoft/yii2/issues/2687 - } - list ($sql, $params) = $db->getQueryBuilder()->build($this); - } else { - $sql = $this->sql; - $params = $this->params; - } - return $db->createCommand($sql, $params); - } - - /** - * Creates a command for lazy loading of a relation. - * @param Connection $db the DB connection used to create the DB command. - * @return Command the created DB command instance. - */ - private function createRelationalCommand($db = null) - { - $where = $this->where; - - if ($this->via instanceof self) { - // via pivot table - $viaModels = $this->via->findPivotRows([$this->primaryModel]); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveQuery $viaQuery */ - list($viaName, $viaQuery) = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - - if (!empty($this->on)) { - $this->andWhere($this->on); - } - - $command = $this->createCommandInternal($db); - - $this->where = $where; - - return $command; - } - - /** - * Joins with the specified relations. - * - * This method allows you to reuse existing relation definitions to perform JOIN queries. - * Based on the definition of the specified relation(s), the method will append one or multiple - * JOIN statements to the current query. - * - * If the `$eagerLoading` parameter is true, the method will also eager loading the specified relations, - * which is equivalent to calling [[with()]] using the specified relations. - * - * Note that because a JOIN query will be performed, you are responsible to disambiguate column names. - * - * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement - * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. - * - * @param array $with the relations to be joined. Each array element represents a single relation. - * The array keys are relation names, and the array values are the corresponding anonymous functions that - * can be used to modify the relation queries on-the-fly. If a relation query does not need modification, - * you may use the relation name as the array value. Sub-relations can also be specified (see [[with()]]). - * For example, - * - * ```php - * // find all orders that contain books, and eager loading "books" - * Order::find()->joinWith('books', true, 'INNER JOIN')->all(); - * // find all orders, eager loading "books", and sort the orders and books by the book names. - * Order::find()->joinWith([ - * 'books' => function ($query) { - * $query->orderBy('tbl_item.name'); - * } - * ])->all(); - * ``` - * - * @param boolean|array $eagerLoading whether to eager load the relations specified in `$with`. - * When this is a boolean, it applies to all relations specified in `$with`. Use an array - * to explicitly list which relations in `$with` need to be eagerly loaded. - * @param string|array $joinType the join type of the relations specified in `$with`. - * When this is a string, it applies to all relations specified in `$with`. Use an array - * in the format of `relationName => joinType` to specify different join types for different relations. - * @return static the query object itself - */ - public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN') - { - $this->joinWith[] = [(array)$with, $eagerLoading, $joinType]; - return $this; - } - - private function buildJoinWith() - { - foreach ($this->joinWith as $config) { - list ($with, $eagerLoading, $joinType) = $config; - $this->joinWithRelations(new $this->modelClass, $with, $joinType); - - if (is_array($eagerLoading)) { - foreach ($with as $name => $callback) { - if (is_integer($name)) { - if (!in_array($callback, $eagerLoading, true)) { - unset($with[$name]); - } - } elseif (!in_array($name, $eagerLoading, true)) { - unset($with[$name]); - } - } - } elseif (!$eagerLoading) { - $with = []; - } - - $this->with($with); - } - } - - /** - * Inner joins with the specified relations. - * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN". - * Please refer to [[joinWith()]] for detailed usage of this method. - * @param array $with the relations to be joined with - * @param boolean|array $eagerLoading whether to eager loading the relations - * @return static the query object itself - * @see joinWith() - */ - public function innerJoinWith($with, $eagerLoading = true) - { - return $this->joinWith($with, $eagerLoading, 'INNER JOIN'); - } - - /** - * Modifies the current query by adding join fragments based on the given relations. - * @param ActiveRecord $model the primary model - * @param array $with the relations to be joined - * @param string|array $joinType the join type - */ - private function joinWithRelations($model, $with, $joinType) - { - $relations = []; - - foreach ($with as $name => $callback) { - if (is_integer($name)) { - $name = $callback; - $callback = null; - } - - $primaryModel = $model; - $parent = $this; - $prefix = ''; - while (($pos = strpos($name, '.')) !== false) { - $childName = substr($name, $pos + 1); - $name = substr($name, 0, $pos); - $fullName = $prefix === '' ? $name : "$prefix.$name"; - if (!isset($relations[$fullName])) { - $relations[$fullName] = $relation = $primaryModel->getRelation($name); - $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName)); - } else { - $relation = $relations[$fullName]; - } - $primaryModel = new $relation->modelClass; - $parent = $relation; - $prefix = $fullName; - $name = $childName; - } - - $fullName = $prefix === '' ? $name : "$prefix.$name"; - if (!isset($relations[$fullName])) { - $relations[$fullName] = $relation = $primaryModel->getRelation($name); - if ($callback !== null) { - call_user_func($callback, $relation); - } - $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName)); - } - } - } - - /** - * Returns the join type based on the given join type parameter and the relation name. - * @param string|array $joinType the given join type(s) - * @param string $name relation name - * @return string the real join type - */ - private function getJoinType($joinType, $name) - { - if (is_array($joinType) && isset($joinType[$name])) { - return $joinType[$name]; - } else { - return is_string($joinType) ? $joinType : 'INNER JOIN'; - } - } - - /** - * Returns the table name and the table alias for [[modelClass]]. - * @param ActiveQuery $query - * @return array the table name and the table alias. - */ - private function getQueryTableName($query) - { - if (empty($query->from)) { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $tableName = $modelClass::tableName(); - } else { - $tableName = ''; - foreach ($query->from as $alias => $tableName) { - if (is_string($alias)) { - return [$tableName, $alias]; - } else { - break; - } - } - } - - if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) { - $alias = $matches[2]; - } else { - $alias = $tableName; - } - - return [$tableName, $alias]; - } - - /** - * Joins a parent query with a child query. - * The current query object will be modified accordingly. - * @param ActiveQuery $parent - * @param ActiveQuery $child - * @param string $joinType - */ - private function joinWithRelation($parent, $child, $joinType) - { - $via = $child->via; - $child->via = null; - if ($via instanceof ActiveQuery) { - // via table - $this->joinWithRelation($parent, $via, $joinType); - $this->joinWithRelation($via, $child, $joinType); - return; - } elseif (is_array($via)) { - // via relation - $this->joinWithRelation($parent, $via[1], $joinType); - $this->joinWithRelation($via[1], $child, $joinType); - return; - } - - list ($parentTable, $parentAlias) = $this->getQueryTableName($parent); - list ($childTable, $childAlias) = $this->getQueryTableName($child); - - if (!empty($child->link)) { - - if (strpos($parentAlias, '{{') === false) { - $parentAlias = '{{' . $parentAlias . '}}'; - } - if (strpos($childAlias, '{{') === false) { - $childAlias = '{{' . $childAlias . '}}'; - } - - $on = []; - foreach ($child->link as $childColumn => $parentColumn) { - $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]"; - } - $on = implode(' AND ', $on); - if (!empty($child->on)) { - $on = ['and', $on, $child->on]; - } - } else { - $on = $child->on; - } - $this->join($joinType, $childTable, $on); - - - if (!empty($child->where)) { - $this->andWhere($child->where); - } - if (!empty($child->having)) { - $this->andHaving($child->having); - } - if (!empty($child->orderBy)) { - $this->addOrderBy($child->orderBy); - } - if (!empty($child->groupBy)) { - $this->addGroupBy($child->groupBy); - } - if (!empty($child->params)) { - $this->addParams($child->params); - } - if (!empty($child->join)) { - foreach ($child->join as $join) { - $this->join[] = $join; - } - } - if (!empty($child->union)) { - foreach ($child->union as $union) { - $this->union[] = $union; - } - } - } - - /** - * Sets the ON condition for a relational query. - * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called. - * Otherwise, the condition will be used in the WHERE part of a query. - * - * Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class: - * - * ```php - * public function getActiveUsers() - * { - * return $this->hasMany(User::className(), ['id' => 'user_id'])->onCondition(['active' => true]); - * } - * ``` - * - * @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - */ - public function onCondition($condition, $params = []) - { - $this->on = $condition; - $this->addParams($params); - return $this; - } - - /** - * Specifies the pivot table for a relational query. - * - * Use this method to specify a pivot table when declaring a relation in the [[ActiveRecord]] class: - * - * ```php - * public function getItems() - * { - * return $this->hasMany(Item::className(), ['id' => 'item_id']) - * ->viaTable('tbl_order_item', ['order_id' => 'id']); - * } - * ``` - * - * @param string $tableName the name of the pivot table. - * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. - * The keys of the array represent the columns in the pivot table, and the values represent the columns - * in the [[primaryModel]] table. - * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. - * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return static - * @see via() - */ - public function viaTable($tableName, $link, $callable = null) - { - $relation = new ActiveQuery([ - 'modelClass' => get_class($this->primaryModel), - 'from' => [$tableName], - 'link' => $link, - 'multiple' => true, - 'asArray' => true, - ]); - $this->via = $relation; - if ($callable !== null) { - call_user_func($callable, $relation); - } - return $this; - } + use ActiveQueryTrait; + use ActiveRelationTrait; + + /** + * @var string the SQL statement to be executed for retrieving AR records. + * This is set by [[ActiveRecord::findBySql()]]. + */ + public $sql; + /** + * @var string|array the join condition to be used when this query is used in a relational context. + * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called. + * Otherwise, the condition will be used in the WHERE part of a query. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @see onCondition() + */ + public $on; + /** + * @var array a list of relations that this query should be joined with + */ + public $joinWith; + + /** + * Executes query and returns all results as an array. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + */ + public function prepareResult($rows) + { + if (empty($rows)) { + return []; + } + + $models = $this->createModels($rows); + if (!empty($this->join) && $this->indexBy === null) { + $models = $this->removeDuplicatedModels($models); + } + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } + + return $models; + } + + /** + * Removes duplicated models by checking their primary key values. + * This method is mainly called when a join query is performed, which may cause duplicated rows being returned. + * @param array $models the models to be checked + * @return array the distinctive models + */ + private function removeDuplicatedModels($models) + { + $hash = []; + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pks = $class::primaryKey(); + + if (count($pks) > 1) { + foreach ($models as $i => $model) { + $key = []; + foreach ($pks as $pk) { + $key[] = $model[$pk]; + } + $key = serialize($key); + if (isset($hash[$key])) { + unset($models[$i]); + } else { + $hash[$key] = true; + } + } + } else { + $pk = reset($pks); + foreach ($models as $i => $model) { + $key = $model[$pk]; + if (isset($hash[$key])) { + unset($models[$i]); + } else { + $hash[$key] = true; + } + } + } + + return array_values($models); + } + + /** + * Executes query and returns a single row of result. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + $command = $this->createCommand($db); + $row = $command->queryOne(); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + if (!$this->asArray) { + $model->afterFind(); + } + + return $model; + } else { + return null; + } + } + + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + if ($this->primaryModel === null) { + // not a relational context or eager loading + if (!empty($this->on)) { + $where = $this->where; + $this->andWhere($this->on); + $command = $this->createCommandInternal($db); + $this->where = $where; + + return $command; + } else { + return $this->createCommandInternal($db); + } + } else { + // lazy loading of a relation + return $this->createRelationalCommand($db); + } + } + + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return Command the created DB command instance. + */ + protected function createCommandInternal($db) + { + /** @var ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); + } + + if ($this->sql === null) { + if (!empty($this->joinWith)) { + $this->buildJoinWith(); + $this->joinWith = null; // clean it up to avoid issue https://github.com/yiisoft/yii2/issues/2687 + } + list ($sql, $params) = $db->getQueryBuilder()->build($this); + } else { + $sql = $this->sql; + $params = $this->params; + } + + return $db->createCommand($sql, $params); + } + + /** + * Creates a command for lazy loading of a relation. + * @param Connection $db the DB connection used to create the DB command. + * @return Command the created DB command instance. + */ + private function createRelationalCommand($db = null) + { + $where = $this->where; + + if ($this->via instanceof self) { + // via pivot table + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveQuery $viaQuery */ + list($viaName, $viaQuery) = $this->via; + if ($viaQuery->multiple) { + $viaModels = $viaQuery->all(); + $this->primaryModel->populateRelation($viaName, $viaModels); + } else { + $model = $viaQuery->one(); + $this->primaryModel->populateRelation($viaName, $model); + $viaModels = $model === null ? [] : [$model]; + } + $this->filterByModels($viaModels); + } else { + $this->filterByModels([$this->primaryModel]); + } + + if (!empty($this->on)) { + $this->andWhere($this->on); + } + + $command = $this->createCommandInternal($db); + + $this->where = $where; + + return $command; + } + + /** + * Joins with the specified relations. + * + * This method allows you to reuse existing relation definitions to perform JOIN queries. + * Based on the definition of the specified relation(s), the method will append one or multiple + * JOIN statements to the current query. + * + * If the `$eagerLoading` parameter is true, the method will also eager loading the specified relations, + * which is equivalent to calling [[with()]] using the specified relations. + * + * Note that because a JOIN query will be performed, you are responsible to disambiguate column names. + * + * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement + * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. + * + * @param array $with the relations to be joined. Each array element represents a single relation. + * The array keys are relation names, and the array values are the corresponding anonymous functions that + * can be used to modify the relation queries on-the-fly. If a relation query does not need modification, + * you may use the relation name as the array value. Sub-relations can also be specified (see [[with()]]). + * For example, + * + * ```php + * // find all orders that contain books, and eager loading "books" + * Order::find()->joinWith('books', true, 'INNER JOIN')->all(); + * // find all orders, eager loading "books", and sort the orders and books by the book names. + * Order::find()->joinWith([ + * 'books' => function ($query) { + * $query->orderBy('tbl_item.name'); + * } + * ])->all(); + * ``` + * + * @param boolean|array $eagerLoading whether to eager load the relations specified in `$with`. + * When this is a boolean, it applies to all relations specified in `$with`. Use an array + * to explicitly list which relations in `$with` need to be eagerly loaded. + * @param string|array $joinType the join type of the relations specified in `$with`. + * When this is a string, it applies to all relations specified in `$with`. Use an array + * in the format of `relationName => joinType` to specify different join types for different relations. + * @return static the query object itself + */ + public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN') + { + $this->joinWith[] = [(array) $with, $eagerLoading, $joinType]; + + return $this; + } + + private function buildJoinWith() + { + foreach ($this->joinWith as $config) { + list ($with, $eagerLoading, $joinType) = $config; + $this->joinWithRelations(new $this->modelClass, $with, $joinType); + + if (is_array($eagerLoading)) { + foreach ($with as $name => $callback) { + if (is_integer($name)) { + if (!in_array($callback, $eagerLoading, true)) { + unset($with[$name]); + } + } elseif (!in_array($name, $eagerLoading, true)) { + unset($with[$name]); + } + } + } elseif (!$eagerLoading) { + $with = []; + } + + $this->with($with); + } + } + + /** + * Inner joins with the specified relations. + * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN". + * Please refer to [[joinWith()]] for detailed usage of this method. + * @param array $with the relations to be joined with + * @param boolean|array $eagerLoading whether to eager loading the relations + * @return static the query object itself + * @see joinWith() + */ + public function innerJoinWith($with, $eagerLoading = true) + { + return $this->joinWith($with, $eagerLoading, 'INNER JOIN'); + } + + /** + * Modifies the current query by adding join fragments based on the given relations. + * @param ActiveRecord $model the primary model + * @param array $with the relations to be joined + * @param string|array $joinType the join type + */ + private function joinWithRelations($model, $with, $joinType) + { + $relations = []; + + foreach ($with as $name => $callback) { + if (is_integer($name)) { + $name = $callback; + $callback = null; + } + + $primaryModel = $model; + $parent = $this; + $prefix = ''; + while (($pos = strpos($name, '.')) !== false) { + $childName = substr($name, $pos + 1); + $name = substr($name, 0, $pos); + $fullName = $prefix === '' ? $name : "$prefix.$name"; + if (!isset($relations[$fullName])) { + $relations[$fullName] = $relation = $primaryModel->getRelation($name); + $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName)); + } else { + $relation = $relations[$fullName]; + } + $primaryModel = new $relation->modelClass; + $parent = $relation; + $prefix = $fullName; + $name = $childName; + } + + $fullName = $prefix === '' ? $name : "$prefix.$name"; + if (!isset($relations[$fullName])) { + $relations[$fullName] = $relation = $primaryModel->getRelation($name); + if ($callback !== null) { + call_user_func($callback, $relation); + } + $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName)); + } + } + } + + /** + * Returns the join type based on the given join type parameter and the relation name. + * @param string|array $joinType the given join type(s) + * @param string $name relation name + * @return string the real join type + */ + private function getJoinType($joinType, $name) + { + if (is_array($joinType) && isset($joinType[$name])) { + return $joinType[$name]; + } else { + return is_string($joinType) ? $joinType : 'INNER JOIN'; + } + } + + /** + * Returns the table name and the table alias for [[modelClass]]. + * @param ActiveQuery $query + * @return array the table name and the table alias. + */ + private function getQueryTableName($query) + { + if (empty($query->from)) { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $tableName = $modelClass::tableName(); + } else { + $tableName = ''; + foreach ($query->from as $alias => $tableName) { + if (is_string($alias)) { + return [$tableName, $alias]; + } else { + break; + } + } + } + + if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) { + $alias = $matches[2]; + } else { + $alias = $tableName; + } + + return [$tableName, $alias]; + } + + /** + * Joins a parent query with a child query. + * The current query object will be modified accordingly. + * @param ActiveQuery $parent + * @param ActiveQuery $child + * @param string $joinType + */ + private function joinWithRelation($parent, $child, $joinType) + { + $via = $child->via; + $child->via = null; + if ($via instanceof ActiveQuery) { + // via table + $this->joinWithRelation($parent, $via, $joinType); + $this->joinWithRelation($via, $child, $joinType); + + return; + } elseif (is_array($via)) { + // via relation + $this->joinWithRelation($parent, $via[1], $joinType); + $this->joinWithRelation($via[1], $child, $joinType); + + return; + } + + list ($parentTable, $parentAlias) = $this->getQueryTableName($parent); + list ($childTable, $childAlias) = $this->getQueryTableName($child); + + if (!empty($child->link)) { + + if (strpos($parentAlias, '{{') === false) { + $parentAlias = '{{' . $parentAlias . '}}'; + } + if (strpos($childAlias, '{{') === false) { + $childAlias = '{{' . $childAlias . '}}'; + } + + $on = []; + foreach ($child->link as $childColumn => $parentColumn) { + $on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]"; + } + $on = implode(' AND ', $on); + if (!empty($child->on)) { + $on = ['and', $on, $child->on]; + } + } else { + $on = $child->on; + } + $this->join($joinType, $childTable, $on); + + if (!empty($child->where)) { + $this->andWhere($child->where); + } + if (!empty($child->having)) { + $this->andHaving($child->having); + } + if (!empty($child->orderBy)) { + $this->addOrderBy($child->orderBy); + } + if (!empty($child->groupBy)) { + $this->addGroupBy($child->groupBy); + } + if (!empty($child->params)) { + $this->addParams($child->params); + } + if (!empty($child->join)) { + foreach ($child->join as $join) { + $this->join[] = $join; + } + } + if (!empty($child->union)) { + foreach ($child->union as $union) { + $this->union[] = $union; + } + } + } + + /** + * Sets the ON condition for a relational query. + * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called. + * Otherwise, the condition will be used in the WHERE part of a query. + * + * Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class: + * + * ```php + * public function getActiveUsers() + * { + * return $this->hasMany(User::className(), ['id' => 'user_id'])->onCondition(['active' => true]); + * } + * ``` + * + * @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + */ + public function onCondition($condition, $params = []) + { + $this->on = $condition; + $this->addParams($params); + + return $this; + } + + /** + * Specifies the pivot table for a relational query. + * + * Use this method to specify a pivot table when declaring a relation in the [[ActiveRecord]] class: + * + * ```php + * public function getItems() + * { + * return $this->hasMany(Item::className(), ['id' => 'item_id']) + * ->viaTable('tbl_order_item', ['order_id' => 'id']); + * } + * ``` + * + * @param string $tableName the name of the pivot table. + * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. + * The keys of the array represent the columns in the pivot table, and the values represent the columns + * in the [[primaryModel]] table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return static + * @see via() + */ + public function viaTable($tableName, $link, $callable = null) + { + $relation = new ActiveQuery([ + 'modelClass' => get_class($this->primaryModel), + 'from' => [$tableName], + 'link' => $link, + 'multiple' => true, + 'asArray' => true, + ]); + $this->via = $relation; + if ($callable !== null) { + call_user_func($callable, $relation); + } + + return $this; + } } diff --git a/framework/db/ActiveQueryInterface.php b/framework/db/ActiveQueryInterface.php index 332d576fe74..7d3e7f19bf2 100644 --- a/framework/db/ActiveQueryInterface.php +++ b/framework/db/ActiveQueryInterface.php @@ -22,78 +22,78 @@ */ interface ActiveQueryInterface extends QueryInterface { - /** - * Sets the [[asArray]] property. - * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return static the query object itself - */ - public function asArray($value = true); + /** + * Sets the [[asArray]] property. + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. + * @return static the query object itself + */ + public function asArray($value = true); - /** - * Sets the [[indexBy]] property. - * @param string|callable $column the name of the column by which the query results should be indexed by. - * This can also be a callable (e.g. anonymous function) that returns the index value based on the given - * row or model data. The signature of the callable should be: - * - * ~~~ - * // $model is an AR instance when `asArray` is false, - * // or an array of column values when `asArray` is true. - * function ($model) - * { - * // return the index value corresponding to $model - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column); + /** + * Sets the [[indexBy]] property. + * @param string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row or model data. The signature of the callable should be: + * + * ~~~ + * // $model is an AR instance when `asArray` is false, + * // or an array of column values when `asArray` is true. + * function ($model) + * { + * // return the index value corresponding to $model + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column); - /** - * Specifies the relations with which this query should be performed. - * - * The parameters to this method can be either one or multiple strings, or a single array - * of relation names and the optional callbacks to customize the relations. - * - * A relation name can refer to a relation defined in [[ActiveQueryTrait::modelClass|modelClass]] - * or a sub-relation that stands for a relation of a related record. - * For example, `orders.address` means the `address` relation defined - * in the model class corresponding to the `orders` relation. - * - * The followings are some usage examples: - * - * ~~~ - * // find customers together with their orders and country - * Customer::find()->with('orders', 'country')->all(); - * // find customers together with their orders and the orders' shipping address - * Customer::find()->with('orders.address')->all(); - * // find customers together with their country and orders of status 1 - * Customer::find()->with([ - * 'orders' => function($query) { - * $query->andWhere('status = 1'); - * }, - * 'country', - * ])->all(); - * ~~~ - * - * @return static the query object itself - */ - public function with(); + /** + * Specifies the relations with which this query should be performed. + * + * The parameters to this method can be either one or multiple strings, or a single array + * of relation names and the optional callbacks to customize the relations. + * + * A relation name can refer to a relation defined in [[ActiveQueryTrait::modelClass|modelClass]] + * or a sub-relation that stands for a relation of a related record. + * For example, `orders.address` means the `address` relation defined + * in the model class corresponding to the `orders` relation. + * + * The followings are some usage examples: + * + * ~~~ + * // find customers together with their orders and country + * Customer::find()->with('orders', 'country')->all(); + * // find customers together with their orders and the orders' shipping address + * Customer::find()->with('orders.address')->all(); + * // find customers together with their country and orders of status 1 + * Customer::find()->with([ + * 'orders' => function ($query) { + * $query->andWhere('status = 1'); + * }, + * 'country', + * ])->all(); + * ~~~ + * + * @return static the query object itself + */ + public function with(); - /** - * Specifies the relation associated with the pivot table for use in relational query. - * @param string $relationName the relation name. This refers to a relation declared in the [[ActiveRelationTrait::primaryModel|primaryModel]] of the relation. - * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. - * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return static the relation object itself. - */ - public function via($relationName, $callable = null); + /** + * Specifies the relation associated with the pivot table for use in relational query. + * @param string $relationName the relation name. This refers to a relation declared in the [[ActiveRelationTrait::primaryModel|primaryModel]] of the relation. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return static the relation object itself. + */ + public function via($relationName, $callable = null); - /** - * Finds the related records for the specified primary record. - * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion. - * @param string $name the relation name - * @param ActiveRecordInterface $model the primary model - * @return mixed the related record(s) - */ - public function findFor($name, $model); + /** + * Finds the related records for the specified primary record. + * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion. + * @param string $name the relation name + * @param ActiveRecordInterface $model the primary model + * @return mixed the related record(s) + */ + public function findFor($name, $model); } diff --git a/framework/db/ActiveQueryTrait.php b/framework/db/ActiveQueryTrait.php index fb2f9f98e75..dd3a4543611 100644 --- a/framework/db/ActiveQueryTrait.php +++ b/framework/db/ActiveQueryTrait.php @@ -16,191 +16,194 @@ */ trait ActiveQueryTrait { - /** - * @var string the name of the ActiveRecord class. - */ - public $modelClass; - /** - * @var array a list of relations that this query should be performed with - */ - public $with; - /** - * @var boolean whether to return each record as an array. If false (default), an object - * of [[modelClass]] will be created to represent each record. - */ - public $asArray; + /** + * @var string the name of the ActiveRecord class. + */ + public $modelClass; + /** + * @var array a list of relations that this query should be performed with + */ + public $with; + /** + * @var boolean whether to return each record as an array. If false (default), an object + * of [[modelClass]] will be created to represent each record. + */ + public $asArray; - /** - * Sets the [[asArray]] property. - * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return static the query object itself - */ - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } + /** + * Sets the [[asArray]] property. + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. + * @return static the query object itself + */ + public function asArray($value = true) + { + $this->asArray = $value; - /** - * Specifies the relations with which this query should be performed. - * - * The parameters to this method can be either one or multiple strings, or a single array - * of relation names and the optional callbacks to customize the relations. - * - * A relation name can refer to a relation defined in [[modelClass]] - * or a sub-relation that stands for a relation of a related record. - * For example, `orders.address` means the `address` relation defined - * in the model class corresponding to the `orders` relation. - * - * The followings are some usage examples: - * - * ~~~ - * // find customers together with their orders and country - * Customer::find()->with('orders', 'country')->all(); - * // find customers together with their orders and the orders' shipping address - * Customer::find()->with('orders.address')->all(); - * // find customers together with their country and orders of status 1 - * Customer::find()->with([ - * 'orders' => function($query) { - * $query->andWhere('status = 1'); - * }, - * 'country', - * ])->all(); - * ~~~ - * - * You can call `with()` multiple times. Each call will add relations to the existing ones. - * For example, the following two statements are equivalent: - * - * ~~~ - * Customer::find()->with('orders', 'country')->all(); - * Customer::find()->with('orders')->with('country')->all(); - * ~~~ - * - * @return static the query object itself - */ - public function with() - { - $with = func_get_args(); - if (isset($with[0]) && is_array($with[0])) { - // the parameter is given as an array - $with = $with[0]; - } + return $this; + } - if (empty($this->with)) { - $this->with = $with; - } elseif (!empty($with)) { - foreach ($with as $name => $value) { - if (is_integer($name)) { - // repeating relation is fine as normalizeRelations() handle it well - $this->with[] = $value; - } else { - $this->with[$name] = $value; - } - } - } + /** + * Specifies the relations with which this query should be performed. + * + * The parameters to this method can be either one or multiple strings, or a single array + * of relation names and the optional callbacks to customize the relations. + * + * A relation name can refer to a relation defined in [[modelClass]] + * or a sub-relation that stands for a relation of a related record. + * For example, `orders.address` means the `address` relation defined + * in the model class corresponding to the `orders` relation. + * + * The followings are some usage examples: + * + * ~~~ + * // find customers together with their orders and country + * Customer::find()->with('orders', 'country')->all(); + * // find customers together with their orders and the orders' shipping address + * Customer::find()->with('orders.address')->all(); + * // find customers together with their country and orders of status 1 + * Customer::find()->with([ + * 'orders' => function ($query) { + * $query->andWhere('status = 1'); + * }, + * 'country', + * ])->all(); + * ~~~ + * + * You can call `with()` multiple times. Each call will add relations to the existing ones. + * For example, the following two statements are equivalent: + * + * ~~~ + * Customer::find()->with('orders', 'country')->all(); + * Customer::find()->with('orders')->with('country')->all(); + * ~~~ + * + * @return static the query object itself + */ + public function with() + { + $with = func_get_args(); + if (isset($with[0]) && is_array($with[0])) { + // the parameter is given as an array + $with = $with[0]; + } - return $this; - } + if (empty($this->with)) { + $this->with = $with; + } elseif (!empty($with)) { + foreach ($with as $name => $value) { + if (is_integer($name)) { + // repeating relation is fine as normalizeRelations() handle it well + $this->with[] = $value; + } else { + $this->with[$name] = $value; + } + } + } - /** - * Converts found rows into model instances - * @param array $rows - * @return array|ActiveRecord[] - */ - private function createModels($rows) - { - $models = []; - if ($this->asArray) { - if ($this->indexBy === null) { - return $rows; - } - foreach ($rows as $row) { - if (is_string($this->indexBy)) { - $key = $row[$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - $models[$key] = $row; - } - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - if ($this->indexBy === null) { - foreach ($rows as $row) { - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - $models[] = $model; - } - } else { - foreach ($rows as $row) { - $model = $class::instantiate($row); - $class::populateRecord($model, $row); - if (is_string($this->indexBy)) { - $key = $model->{$this->indexBy}; - } else { - $key = call_user_func($this->indexBy, $model); - } - $models[$key] = $model; - } - } - } - return $models; - } + return $this; + } - /** - * Finds records corresponding to one or multiple relations and populates them into the primary models. - * @param array $with a list of relations that this query should be performed with. Please - * refer to [[with()]] for details about specifying this parameter. - * @param array|ActiveRecord[] $models the primary models (can be either AR instances or arrays) - */ - public function findWith($with, &$models) - { - $primaryModel = new $this->modelClass; - $relations = $this->normalizeRelations($primaryModel, $with); - foreach ($relations as $name => $relation) { - if ($relation->asArray === null) { - // inherit asArray from primary query - $relation->asArray = $this->asArray; - } - $relation->populateRelation($name, $models); - } - } + /** + * Converts found rows into model instances + * @param array $rows + * @return array|ActiveRecord[] + */ + private function createModels($rows) + { + $models = []; + if ($this->asArray) { + if ($this->indexBy === null) { + return $rows; + } + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $models[$key] = $row; + } + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + $models[] = $model; + } + } else { + foreach ($rows as $row) { + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + if (is_string($this->indexBy)) { + $key = $model->{$this->indexBy}; + } else { + $key = call_user_func($this->indexBy, $model); + } + $models[$key] = $model; + } + } + } - /** - * @param ActiveRecord $model - * @param array $with - * @return ActiveQueryInterface[] - */ - private function normalizeRelations($model, $with) - { - $relations = []; - foreach ($with as $name => $callback) { - if (is_integer($name)) { - $name = $callback; - $callback = null; - } - if (($pos = strpos($name, '.')) !== false) { - // with sub-relations - $childName = substr($name, $pos + 1); - $name = substr($name, 0, $pos); - } else { - $childName = null; - } + return $models; + } - if (!isset($relations[$name])) { - $relation = $model->getRelation($name); - $relation->primaryModel = null; - $relations[$name] = $relation; - } else { - $relation = $relations[$name]; - } + /** + * Finds records corresponding to one or multiple relations and populates them into the primary models. + * @param array $with a list of relations that this query should be performed with. Please + * refer to [[with()]] for details about specifying this parameter. + * @param array|ActiveRecord[] $models the primary models (can be either AR instances or arrays) + */ + public function findWith($with, &$models) + { + $primaryModel = new $this->modelClass; + $relations = $this->normalizeRelations($primaryModel, $with); + foreach ($relations as $name => $relation) { + if ($relation->asArray === null) { + // inherit asArray from primary query + $relation->asArray = $this->asArray; + } + $relation->populateRelation($name, $models); + } + } - if (isset($childName)) { - $relation->with[$childName] = $callback; - } elseif ($callback !== null) { - call_user_func($callback, $relation); - } - } - return $relations; - } + /** + * @param ActiveRecord $model + * @param array $with + * @return ActiveQueryInterface[] + */ + private function normalizeRelations($model, $with) + { + $relations = []; + foreach ($with as $name => $callback) { + if (is_integer($name)) { + $name = $callback; + $callback = null; + } + if (($pos = strpos($name, '.')) !== false) { + // with sub-relations + $childName = substr($name, $pos + 1); + $name = substr($name, 0, $pos); + } else { + $childName = null; + } + + if (!isset($relations[$name])) { + $relation = $model->getRelation($name); + $relation->primaryModel = null; + $relations[$name] = $relation; + } else { + $relation = $relations[$name]; + } + + if (isset($childName)) { + $relation->with[$childName] = $callback; + } elseif ($callback !== null) { + call_user_func($callback, $relation); + } + } + + return $relations; + } } diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 539508ba868..74b22f89be5 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -74,533 +74,544 @@ */ class ActiveRecord extends BaseActiveRecord { - /** - * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. - */ - const OP_INSERT = 0x01; - /** - * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. - */ - const OP_UPDATE = 0x02; - /** - * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. - */ - const OP_DELETE = 0x04; - /** - * All three operations: insert, update, delete. - * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE. - */ - const OP_ALL = 0x07; - - /** - * Returns the database connection used by this AR class. - * By default, the "db" application component is used as the database connection. - * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->getDb(); - } - - /** - * Creates an [[ActiveQuery]] instance with a given SQL statement. - * - * Note that because the SQL statement is already specified, calling additional - * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]] - * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is - * still fine. - * - * Below is an example: - * - * ~~~ - * $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); - * ~~~ - * - * @param string $sql the SQL statement to be executed - * @param array $params parameters to be bound to the SQL statement during execution. - * @return ActiveQuery the newly created [[ActiveQuery]] instance - */ - public static function findBySql($sql, $params = []) - { - $query = static::createQuery(); - $query->sql = $sql; - return $query->params($params); - } - - /** - * Updates the whole table using the provided attribute values and conditions. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], 'status = 2'); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = '', $params = []) - { - $command = static::getDb()->createCommand(); - $command->update(static::tableName(), $attributes, $condition, $params); - return $command->execute(); - } - - /** - * Updates the whole table using the provided counter changes and conditions. - * For example, to increment all customers' age by 1, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method. - * @return integer the number of rows updated - */ - public static function updateAllCounters($counters, $condition = '', $params = []) - { - $n = 0; - foreach ($counters as $name => $value) { - $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]); - $n++; - } - $command = static::getDb()->createCommand(); - $command->update(static::tableName(), $counters, $condition, $params); - return $command->execute(); - } - - /** - * Deletes rows in the table using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll('status = 3'); - * ~~~ - * - * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = '', $params = []) - { - $command = static::getDb()->createCommand(); - $command->delete(static::tableName(), $condition, $params); - return $command->execute(); - } - - /** - * Creates an [[ActiveQuery]] instance. - * - * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also - * by [[hasOne()]] and [[hasMany()]] to create a relational query. - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ActiveQuery($config); - } - - /** - * Declares the name of the database table associated with this AR class. - * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]] - * with prefix [[Connection::tablePrefix]]. For example if [[Connection::tablePrefix]] is 'tbl_', - * 'Customer' becomes 'tbl_customer', and 'OrderItem' becomes 'tbl_order_item'. You may override this method - * if the table is not named after this convention. - * @return string the table name - */ - public static function tableName() - { - return '{{%' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_') . '}}'; - } - - /** - * Returns the schema information of the DB table associated with this AR class. - * @return TableSchema the schema information of the DB table associated with this AR class. - * @throws InvalidConfigException if the table for the AR class does not exist. - */ - public static function getTableSchema() - { - $schema = static::getDb()->getTableSchema(static::tableName()); - if ($schema !== null) { - return $schema; - } else { - throw new InvalidConfigException("The table does not exist: " . static::tableName()); - } - } - - /** - * Returns the primary key name(s) for this AR class. - * The default implementation will return the primary key(s) as declared - * in the DB table that is associated with this AR class. - * - * If the DB table does not declare any primary key, you should override - * this method to return the attributes that you want to use as primary keys - * for this AR class. - * - * Note that an array should be returned even for a table with single primary key. - * - * @return string[] the primary keys of the associated database table. - */ - public static function primaryKey() - { - return static::getTableSchema()->primaryKey; - } - - /** - * Returns the list of all attribute names of the model. - * The default implementation will return all column names of the table associated with this AR class. - * @return array list of attribute names. - */ - public function attributes() - { - return array_keys(static::getTableSchema()->columns); - } - - /** - * Declares which DB operations should be performed within a transaction in different scenarios. - * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]], - * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively. - * By default, these methods are NOT enclosed in a DB transaction. - * - * In some scenarios, to ensure data consistency, you may want to enclose some or all of them - * in transactions. You can do so by overriding this method and returning the operations - * that need to be transactional. For example, - * - * ~~~ - * return [ - * 'admin' => self::OP_INSERT, - * 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, - * // the above is equivalent to the following: - * // 'api' => self::OP_ALL, - * - * ]; - * ~~~ - * - * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]]) - * should be done in a transaction; and in the "api" scenario, all the operations should be done - * in a transaction. - * - * @return array the declarations of transactional operations. The array keys are scenarios names, - * and the array values are the corresponding transaction operations. - */ - public function transactions() - { - return []; - } - - /** - * @inheritdoc - */ - public static function populateRecord($record, $row) - { - $columns = static::getTableSchema()->columns; - foreach ($row as $name => $value) { - if (isset($columns[$name])) { - $row[$name] = $columns[$name]->typecast($value); - } - } - parent::populateRecord($record, $row); - } - - /** - * Inserts a row into the associated database table using the attribute values of this record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. insert the record into database. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. - * - * If the table's primary key is auto-incremental and is null during insertion, - * it will be populated with the actual value after insertion. - * - * For example, to insert a customer record: - * - * ~~~ - * $customer = new Customer; - * $customer->name = $name; - * $customer->email = $email; - * $customer->insert(); - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is inserted successfully. - * @throws \Exception in case insert failed. - */ - public function insert($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - $db = static::getDb(); - if ($this->isTransactional(self::OP_INSERT)) { - $transaction = $db->beginTransaction(); - try { - $result = $this->insertInternal($attributes); - if ($result === false) { - $transaction->rollBack(); - } else { - $transaction->commit(); - } - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } else { - $result = $this->insertInternal($attributes); - } - return $result; - } - - /** - * Inserts an ActiveRecord into DB without considering transaction. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the record is inserted successfully. - */ - protected function insertInternal($attributes = null) - { - if (!$this->beforeSave(true)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - foreach ($this->getPrimaryKey(true) as $key => $value) { - $values[$key] = $value; - } - } - $db = static::getDb(); - $command = $db->createCommand()->insert($this->tableName(), $values); - if (!$command->execute()) { - return false; - } - $table = $this->getTableSchema(); - if ($table->sequenceName !== null) { - foreach ($table->primaryKey as $name) { - if ($this->getAttribute($name) === null) { - $id = $db->getLastInsertID($table->sequenceName); - $this->setAttribute($name, $id); - $this->setOldAttribute($name, $id); - break; - } - } - } - foreach ($values as $name => $value) { - $this->setOldAttribute($name, $value); - } - $this->afterSave(true); - return true; - } - - /** - * Saves the changes to this active record into the associated database table. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. save the record into database. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[dirtyAttributes|changed attribute values]] will be saved into database. - * - * For example, to update a customer record: - * - * ~~~ - * $customer = Customer::find($id); - * $customer->name = $name; - * $customer->email = $email; - * $customer->update(); - * ~~~ - * - * Note that it is possible the update does not affect any row in the table. - * In this case, this method will return 0. For this reason, you should use the following - * code to check if update() is successful or not: - * - * ~~~ - * if ($this->update() !== false) { - * // update successful - * } else { - * // update failed - * } - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return integer|boolean the number of rows affected, or false if validation fails - * or [[beforeSave()]] stops the updating process. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being updated is outdated. - * @throws \Exception in case update failed. - */ - public function update($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - $db = static::getDb(); - if ($this->isTransactional(self::OP_UPDATE)) { - $transaction = $db->beginTransaction(); - try { - $result = $this->updateInternal($attributes); - if ($result === false) { - $transaction->rollBack(); - } else { - $transaction->commit(); - } - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } else { - $result = $this->updateInternal($attributes); - } - return $result; - } - - /** - * Deletes the table row corresponding to this active record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeDelete()]]. If the method returns false, it will skip the - * rest of the steps; - * 2. delete the record from the database; - * 3. call [[afterDelete()]]. - * - * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] - * will be raised by the corresponding methods. - * - * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being deleted is outdated. - * @throws \Exception in case delete failed. - */ - public function delete() - { - $db = static::getDb(); - if ($this->isTransactional(self::OP_DELETE)) { - $transaction = $db->beginTransaction(); - try { - $result = $this->deleteInternal(); - if ($result === false) { - $transaction->rollBack(); - } else { - $transaction->commit(); - } - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } else { - $result = $this->deleteInternal(); - } - - return $result; - } - - /** - * Deletes an ActiveRecord without considering transaction. - * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. - * @throws StaleObjectException - */ - protected function deleteInternal() - { - $result = false; - if ($this->beforeDelete()) { - // we do not check the return value of deleteAll() because it's possible - // the record is already deleted in the database and thus the method will return 0 - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - $condition[$lock] = $this->$lock; - } - $result = $this->deleteAll($condition); - if ($lock !== null && !$result) { - throw new StaleObjectException('The object being deleted is outdated.'); - } - $this->setOldAttributes(null); - $this->afterDelete(); - } - return $result; - } - - /** - * Returns a value indicating whether the given active record is the same as the current one. - * The comparison is made by comparing the table names and the primary key values of the two active records. - * If one of the records [[isNewRecord|is new]] they are also considered not equal. - * @param ActiveRecord $record record to compare to - * @return boolean whether the two active records refer to the same row in the same database table. - */ - public function equals($record) - { - if ($this->isNewRecord || $record->isNewRecord) { - return false; - } - return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey(); - } - - /** - * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. - * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. - * @return boolean whether the specified operation is transactional in the current [[scenario]]. - */ - public function isTransactional($operation) - { - $scenario = $this->getScenario(); - $transactions = $this->transactions(); - return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); - } + /** + * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_INSERT = 0x01; + /** + * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_UPDATE = 0x02; + /** + * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. + */ + const OP_DELETE = 0x04; + /** + * All three operations: insert, update, delete. + * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE. + */ + const OP_ALL = 0x07; + + /** + * Returns the database connection used by this AR class. + * By default, the "db" application component is used as the database connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getDb(); + } + + /** + * Creates an [[ActiveQuery]] instance with a given SQL statement. + * + * Note that because the SQL statement is already specified, calling additional + * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]] + * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is + * still fine. + * + * Below is an example: + * + * ~~~ + * $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); + * ~~~ + * + * @param string $sql the SQL statement to be executed + * @param array $params parameters to be bound to the SQL statement during execution. + * @return ActiveQuery the newly created [[ActiveQuery]] instance + */ + public static function findBySql($sql, $params = []) + { + $query = static::createQuery(); + $query->sql = $sql; + + return $query->params($params); + } + + /** + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], 'status = 2'); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = '', $params = []) + { + $command = static::getDb()->createCommand(); + $command->update(static::tableName(), $attributes, $condition, $params); + + return $command->execute(); + } + + /** + * Updates the whole table using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = '', $params = []) + { + $n = 0; + foreach ($counters as $name => $value) { + $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]); + $n++; + } + $command = static::getDb()->createCommand(); + $command->update(static::tableName(), $counters, $condition, $params); + + return $command->execute(); + } + + /** + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll('status = 3'); + * ~~~ + * + * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = '', $params = []) + { + $command = static::getDb()->createCommand(); + $command->delete(static::tableName(), $condition, $params); + + return $command->execute(); + } + + /** + * Creates an [[ActiveQuery]] instance. + * + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query but also + * by [[hasOne()]] and [[hasMany()]] to create a relational query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQuery the newly created [[ActiveQuery]] instance. + */ + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new ActiveQuery($config); + } + + /** + * Declares the name of the database table associated with this AR class. + * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]] + * with prefix [[Connection::tablePrefix]]. For example if [[Connection::tablePrefix]] is 'tbl_', + * 'Customer' becomes 'tbl_customer', and 'OrderItem' becomes 'tbl_order_item'. You may override this method + * if the table is not named after this convention. + * @return string the table name + */ + public static function tableName() + { + return '{{%' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_') . '}}'; + } + + /** + * Returns the schema information of the DB table associated with this AR class. + * @return TableSchema the schema information of the DB table associated with this AR class. + * @throws InvalidConfigException if the table for the AR class does not exist. + */ + public static function getTableSchema() + { + $schema = static::getDb()->getTableSchema(static::tableName()); + if ($schema !== null) { + return $schema; + } else { + throw new InvalidConfigException("The table does not exist: " . static::tableName()); + } + } + + /** + * Returns the primary key name(s) for this AR class. + * The default implementation will return the primary key(s) as declared + * in the DB table that is associated with this AR class. + * + * If the DB table does not declare any primary key, you should override + * this method to return the attributes that you want to use as primary keys + * for this AR class. + * + * Note that an array should be returned even for a table with single primary key. + * + * @return string[] the primary keys of the associated database table. + */ + public static function primaryKey() + { + return static::getTableSchema()->primaryKey; + } + + /** + * Returns the list of all attribute names of the model. + * The default implementation will return all column names of the table associated with this AR class. + * @return array list of attribute names. + */ + public function attributes() + { + return array_keys(static::getTableSchema()->columns); + } + + /** + * Declares which DB operations should be performed within a transaction in different scenarios. + * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]], + * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively. + * By default, these methods are NOT enclosed in a DB transaction. + * + * In some scenarios, to ensure data consistency, you may want to enclose some or all of them + * in transactions. You can do so by overriding this method and returning the operations + * that need to be transactional. For example, + * + * ~~~ + * return [ + * 'admin' => self::OP_INSERT, + * 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, + * // the above is equivalent to the following: + * // 'api' => self::OP_ALL, + * + * ]; + * ~~~ + * + * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]]) + * should be done in a transaction; and in the "api" scenario, all the operations should be done + * in a transaction. + * + * @return array the declarations of transactional operations. The array keys are scenarios names, + * and the array values are the corresponding transaction operations. + */ + public function transactions() + { + return []; + } + + /** + * @inheritdoc + */ + public static function populateRecord($record, $row) + { + $columns = static::getTableSchema()->columns; + foreach ($row as $name => $value) { + if (isset($columns[$name])) { + $row[$name] = $columns[$name]->typecast($value); + } + } + parent::populateRecord($record, $row); + } + + /** + * Inserts a row into the associated database table using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. + * + * If the table's primary key is auto-incremental and is null during insertion, + * it will be populated with the actual value after insertion. + * + * For example, to insert a customer record: + * + * ~~~ + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws \Exception in case insert failed. + */ + public function insert($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $db = static::getDb(); + if ($this->isTransactional(self::OP_INSERT)) { + $transaction = $db->beginTransaction(); + try { + $result = $this->insertInternal($attributes); + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + } catch (\Exception $e) { + $transaction->rollBack(); + throw $e; + } + } else { + $result = $this->insertInternal($attributes); + } + + return $result; + } + + /** + * Inserts an ActiveRecord into DB without considering transaction. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the record is inserted successfully. + */ + protected function insertInternal($attributes = null) + { + if (!$this->beforeSave(true)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + foreach ($this->getPrimaryKey(true) as $key => $value) { + $values[$key] = $value; + } + } + $db = static::getDb(); + $command = $db->createCommand()->insert($this->tableName(), $values); + if (!$command->execute()) { + return false; + } + $table = $this->getTableSchema(); + if ($table->sequenceName !== null) { + foreach ($table->primaryKey as $name) { + if ($this->getAttribute($name) === null) { + $id = $db->getLastInsertID($table->sequenceName); + $this->setAttribute($name, $id); + $this->setOldAttribute($name, $id); + break; + } + } + } + foreach ($values as $name => $value) { + $this->setOldAttribute($name, $value); + } + $this->afterSave(true); + + return true; + } + + /** + * Saves the changes to this active record into the associated database table. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. save the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be saved into database. + * + * For example, to update a customer record: + * + * ~~~ + * $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->update(); + * ~~~ + * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being updated is outdated. + * @throws \Exception in case update failed. + */ + public function update($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $db = static::getDb(); + if ($this->isTransactional(self::OP_UPDATE)) { + $transaction = $db->beginTransaction(); + try { + $result = $this->updateInternal($attributes); + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + } catch (\Exception $e) { + $transaction->rollBack(); + throw $e; + } + } else { + $result = $this->updateInternal($attributes); + } + + return $result; + } + + /** + * Deletes the table row corresponding to this active record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeDelete()]]. If the method returns false, it will skip the + * rest of the steps; + * 2. delete the record from the database; + * 3. call [[afterDelete()]]. + * + * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] + * will be raised by the corresponding methods. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. + * @throws \Exception in case delete failed. + */ + public function delete() + { + $db = static::getDb(); + if ($this->isTransactional(self::OP_DELETE)) { + $transaction = $db->beginTransaction(); + try { + $result = $this->deleteInternal(); + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + } catch (\Exception $e) { + $transaction->rollBack(); + throw $e; + } + } else { + $result = $this->deleteInternal(); + } + + return $result; + } + + /** + * Deletes an ActiveRecord without considering transaction. + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException + */ + protected function deleteInternal() + { + $result = false; + if ($this->beforeDelete()) { + // we do not check the return value of deleteAll() because it's possible + // the record is already deleted in the database and thus the method will return 0 + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = $this->deleteAll($condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->setOldAttributes(null); + $this->afterDelete(); + } + + return $result; + } + + /** + * Returns a value indicating whether the given active record is the same as the current one. + * The comparison is made by comparing the table names and the primary key values of the two active records. + * If one of the records [[isNewRecord|is new]] they are also considered not equal. + * @param ActiveRecord $record record to compare to + * @return boolean whether the two active records refer to the same row in the same database table. + */ + public function equals($record) + { + if ($this->isNewRecord || $record->isNewRecord) { + return false; + } + + return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey(); + } + + /** + * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. + * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. + * @return boolean whether the specified operation is transactional in the current [[scenario]]. + */ + public function isTransactional($operation) + { + $scenario = $this->getScenario(); + $transactions = $this->transactions(); + + return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); + } } diff --git a/framework/db/ActiveRecordInterface.php b/framework/db/ActiveRecordInterface.php index 550ae25e752..31b0e097eee 100644 --- a/framework/db/ActiveRecordInterface.php +++ b/framework/db/ActiveRecordInterface.php @@ -1,7 +1,7 @@ */ @@ -16,298 +16,298 @@ */ interface ActiveRecordInterface { - /** - * Returns the primary key **name(s)** for this AR class. - * - * Note that an array should be returned even when the record only has a single primary key. - * - * For the primary key **value** see [[getPrimaryKey()]] instead. - * - * @return string[] the primary key name(s) for this AR class. - */ - public static function primaryKey(); + /** + * Returns the primary key **name(s)** for this AR class. + * + * Note that an array should be returned even when the record only has a single primary key. + * + * For the primary key **value** see [[getPrimaryKey()]] instead. + * + * @return string[] the primary key name(s) for this AR class. + */ + public static function primaryKey(); - /** - * Returns the list of all attribute names of the record. - * @return array list of attribute names. - */ - public function attributes(); + /** + * Returns the list of all attribute names of the record. + * @return array list of attribute names. + */ + public function attributes(); - /** - * Returns the named attribute value. - * If this record is the result of a query and the attribute is not loaded, - * null will be returned. - * @param string $name the attribute name - * @return mixed the attribute value. Null if the attribute is not set or does not exist. - * @see hasAttribute() - */ - public function getAttribute($name); + /** + * Returns the named attribute value. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * @param string $name the attribute name + * @return mixed the attribute value. Null if the attribute is not set or does not exist. + * @see hasAttribute() + */ + public function getAttribute($name); - /** - * Sets the named attribute value. - * @param string $name the attribute name. - * @param mixed $value the attribute value. - * @see hasAttribute() - */ - public function setAttribute($name, $value); + /** + * Sets the named attribute value. + * @param string $name the attribute name. + * @param mixed $value the attribute value. + * @see hasAttribute() + */ + public function setAttribute($name, $value); - /** - * Returns a value indicating whether the record has an attribute with the specified name. - * @param string $name the name of the attribute - * @return boolean whether the record has an attribute with the specified name. - */ - public function hasAttribute($name); + /** + * Returns a value indicating whether the record has an attribute with the specified name. + * @param string $name the name of the attribute + * @return boolean whether the record has an attribute with the specified name. + */ + public function hasAttribute($name); - /** - * Returns the primary key value(s). - * @param boolean $asArray whether to return the primary key value as an array. If true, - * the return value will be an array with attribute names as keys and attribute values as values. - * Note that for composite primary keys, an array will always be returned regardless of this parameter value. - * @return mixed the primary key value. An array (attribute name => attribute value) is returned if the primary key - * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if - * the key value is null). - */ - public function getPrimaryKey($asArray = false); + /** + * Returns the primary key value(s). + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with attribute names as keys and attribute values as values. + * Note that for composite primary keys, an array will always be returned regardless of this parameter value. + * @return mixed the primary key value. An array (attribute name => attribute value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getPrimaryKey($asArray = false); - /** - * Returns the old primary key value(s). - * This refers to the primary key value that is populated into the record - * after executing a find method (e.g. find(), findAll()). - * The value remains unchanged even if the primary key attribute is manually assigned with a different value. - * @param boolean $asArray whether to return the primary key value as an array. If true, - * the return value will be an array with column name as key and column value as value. - * If this is false (default), a scalar value will be returned for non-composite primary key. - * @property mixed The old primary key value. An array (column name => column value) is - * returned if the primary key is composite. A string is returned otherwise (null will be - * returned if the key value is null). - * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key - * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if - * the key value is null). - */ - public function getOldPrimaryKey($asArray = false); + /** + * Returns the old primary key value(s). + * This refers to the primary key value that is populated into the record + * after executing a find method (e.g. find(), findAll()). + * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column name as key and column value as value. + * If this is false (default), a scalar value will be returned for non-composite primary key. + * @property mixed The old primary key value. An array (column name => column value) is + * returned if the primary key is composite. A string is returned otherwise (null will be + * returned if the key value is null). + * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getOldPrimaryKey($asArray = false); - /** - * Returns a value indicating whether the given set of attributes represents the primary key for this model - * @param array $keys the set of attributes to check - * @return boolean whether the given set of attributes represents the primary key for this model - */ - public static function isPrimaryKey($keys); + /** + * Returns a value indicating whether the given set of attributes represents the primary key for this model + * @param array $keys the set of attributes to check + * @return boolean whether the given set of attributes represents the primary key for this model + */ + public static function isPrimaryKey($keys); - /** - * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose. - * - * This method is usually ment to be used like this: - * - * ```php - * Customer::find(1); // find one customer by primary key - * Customer::find()->all(); // find all customers - * ``` - * - * @param mixed $q the query parameter. This can be one of the followings: - * - * - a scalar value (integer or string): query by a single primary key value and return the - * corresponding record. - * - an array of name-value pairs: query by a set of attribute values and return a single record matching all of them. - * - null (not specified): return a new [[ActiveQuery]] object for further query purpose. - * - * @return ActiveQueryInterface|static|null When `$q` is null, a new [[ActiveQuery]] instance - * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be - * returned (null will be returned if there is no matching). - */ - public static function find($q = null); + /** + * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose. + * + * This method is usually ment to be used like this: + * + * ```php + * Customer::find(1); // find one customer by primary key + * Customer::find()->all(); // find all customers + * ``` + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value and return the + * corresponding record. + * - an array of name-value pairs: query by a set of attribute values and return a single record matching all of them. + * - null (not specified): return a new [[ActiveQuery]] object for further query purpose. + * + * @return ActiveQueryInterface|static|null When `$q` is null, a new [[ActiveQuery]] instance + * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be + * returned (null will be returned if there is no matching). + */ + public static function find($q = null); - /** - * Creates an [[ActiveQueryInterface|ActiveQuery]] instance. - * - * This method is called by [[find()]] to start a SELECT query but also - * by [[BaseActiveRecord::hasOne()]] and [[BaseActiveRecord::hasMany()]] to - * create a relational query. - * - * You may override this method to return a customized query (e.g. `CustomerQuery` specified - * written for querying `Customer` purpose.) - * - * You may also define default conditions that should apply to all queries unless overridden: - * - * ```php - * public static function createQuery($config = []) - * { - * return parent::createQuery($config)->where(['deleted' => false]); - * } - * ``` - * - * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the - * default condition. Using [[Query::where()]] will override the default condition. - * - * @param array $config the configuration passed to the ActiveQuery class. - * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance. - */ - public static function createQuery($config = []); + /** + * Creates an [[ActiveQueryInterface|ActiveQuery]] instance. + * + * This method is called by [[find()]] to start a SELECT query but also + * by [[BaseActiveRecord::hasOne()]] and [[BaseActiveRecord::hasMany()]] to + * create a relational query. + * + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * + * You may also define default conditions that should apply to all queries unless overridden: + * + * ```php + * public static function createQuery($config = []) + * { + * return parent::createQuery($config)->where(['deleted' => false]); + * } + * ``` + * + * Note that all queries should use [[Query::andWhere()]] and [[Query::orWhere()]] to keep the + * default condition. Using [[Query::where()]] will override the default condition. + * + * @param array $config the configuration passed to the ActiveQuery class. + * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance. + */ + public static function createQuery($config = []); - /** - * Updates records using the provided attribute values and conditions. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], ['status' => '2']); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved for the record. - * Unlike [[update()]] these are not going to be validated. - * @param array $condition the condition that matches the records that should get updated. - * Please refer to [[QueryInterface::where()]] on how to specify this parameter. - * An empty condition will match all records. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = null); + /** + * Updates records using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], ['status' => '2']); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved for the record. + * Unlike [[update()]] these are not going to be validated. + * @param array $condition the condition that matches the records that should get updated. + * Please refer to [[QueryInterface::where()]] on how to specify this parameter. + * An empty condition will match all records. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = null); - /** - * Deletes records using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll([status = 3]); - * ~~~ - * - * @param array $condition the condition that matches the records that should get deleted. - * Please refer to [[QueryInterface::where()]] on how to specify this parameter. - * An empty condition will match all records. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = null); + /** + * Deletes records using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll([status = 3]); + * ~~~ + * + * @param array $condition the condition that matches the records that should get deleted. + * Please refer to [[QueryInterface::where()]] on how to specify this parameter. + * An empty condition will match all records. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = null); - /** - * Saves the current record. - * - * This method will call [[insert()]] when [[getIsNewRecord|isNewRecord]] is true, or [[update()]] - * when [[getIsNewRecord|isNewRecord]] is false. - * - * For example, to save a customer record: - * - * ~~~ - * $customer = new Customer; // or $customer = Customer::find($id); - * $customer->name = $name; - * $customer->email = $email; - * $customer->save(); - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be saved to database. `false` will be returned - * in this case. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the saving succeeds - */ - public function save($runValidation = true, $attributes = null); + /** + * Saves the current record. + * + * This method will call [[insert()]] when [[getIsNewRecord|isNewRecord]] is true, or [[update()]] + * when [[getIsNewRecord|isNewRecord]] is false. + * + * For example, to save a customer record: + * + * ~~~ + * $customer = new Customer; // or $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->save(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. `false` will be returned + * in this case. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation = true, $attributes = null); - /** - * Inserts the record into the database using the attribute values of this record. - * - * Usage example: - * - * ```php - * $customer = new Customer; - * $customer->name = $name; - * $customer->email = $email; - * $customer->insert(); - * ``` - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is inserted successfully. - */ - public function insert($runValidation = true, $attributes = null); + /** + * Inserts the record into the database using the attribute values of this record. + * + * Usage example: + * + * ```php + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ``` + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + */ + public function insert($runValidation = true, $attributes = null); - /** - * Saves the changes to this active record into the database. - * - * Usage example: - * - * ```php - * $customer = Customer::find($id); - * $customer->name = $name; - * $customer->email = $email; - * $customer->update(); - * ``` - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return integer|boolean the number of rows affected, or false if validation fails - * or updating process is stopped for other reasons. - * Note that it is possible that the number of rows affected is 0, even though the - * update execution is successful. - */ - public function update($runValidation = true, $attributes = null); + /** + * Saves the changes to this active record into the database. + * + * Usage example: + * + * ```php + * $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->update(); + * ``` + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return integer|boolean the number of rows affected, or false if validation fails + * or updating process is stopped for other reasons. + * Note that it is possible that the number of rows affected is 0, even though the + * update execution is successful. + */ + public function update($runValidation = true, $attributes = null); - /** - * Deletes the record from the database. - * - * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible that the number of rows deleted is 0, even though the deletion execution is successful. - */ - public function delete(); + /** + * Deletes the record from the database. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible that the number of rows deleted is 0, even though the deletion execution is successful. + */ + public function delete(); - /** - * Returns a value indicating whether the current record is new (not saved in the database). - * @return boolean whether the record is new and should be inserted when calling [[save()]]. - */ - public function getIsNewRecord(); + /** + * Returns a value indicating whether the current record is new (not saved in the database). + * @return boolean whether the record is new and should be inserted when calling [[save()]]. + */ + public function getIsNewRecord(); - /** - * Returns a value indicating whether the given active record is the same as the current one. - * Two [[isNewRecord|new]] records are considered to be not equal. - * @param static $record record to compare to - * @return boolean whether the two active records refer to the same row in the same database table. - */ - public function equals($record); + /** + * Returns a value indicating whether the given active record is the same as the current one. + * Two [[isNewRecord|new]] records are considered to be not equal. + * @param static $record record to compare to + * @return boolean whether the two active records refer to the same row in the same database table. + */ + public function equals($record); - /** - * Returns the relation object with the specified name. - * A relation is defined by a getter method which returns an object implementing the [[ActiveQueryInterface]] - * (normally this would be a relational [[ActiveQuery]] object). - * It can be declared in either the ActiveRecord class itself or one of its behaviors. - * @param string $name the relation name - * @param boolean $throwException whether to throw exception if the relation does not exist. - * @return ActiveQueryInterface the relational query object - */ - public function getRelation($name, $throwException = true); + /** + * Returns the relation object with the specified name. + * A relation is defined by a getter method which returns an object implementing the [[ActiveQueryInterface]] + * (normally this would be a relational [[ActiveQuery]] object). + * It can be declared in either the ActiveRecord class itself or one of its behaviors. + * @param string $name the relation name + * @param boolean $throwException whether to throw exception if the relation does not exist. + * @return ActiveQueryInterface the relational query object + */ + public function getRelation($name, $throwException = true); - /** - * Establishes the relationship between two records. - * - * The relationship is established by setting the foreign key value(s) in one record - * to be the corresponding primary key value(s) in the other record. - * The record with the foreign key will be saved into database without performing validation. - * - * If the relationship involves a pivot table, a new row will be inserted into the - * pivot table which contains the primary key values from both records. - * - * This method requires that the primary key value is not null. - * - * @param string $name the case sensitive name of the relationship. - * @param static $model the record to be linked with the current one. - * @param array $extraColumns additional column values to be saved into the pivot table. - * This parameter is only meaningful for a relationship involving a pivot table - * (i.e., a relation set with `[[ActiveQueryInterface::via()]]`.) - */ - public function link($name, $model, $extraColumns = []); + /** + * Establishes the relationship between two records. + * + * The relationship is established by setting the foreign key value(s) in one record + * to be the corresponding primary key value(s) in the other record. + * The record with the foreign key will be saved into database without performing validation. + * + * If the relationship involves a pivot table, a new row will be inserted into the + * pivot table which contains the primary key values from both records. + * + * This method requires that the primary key value is not null. + * + * @param string $name the case sensitive name of the relationship. + * @param static $model the record to be linked with the current one. + * @param array $extraColumns additional column values to be saved into the pivot table. + * This parameter is only meaningful for a relationship involving a pivot table + * (i.e., a relation set with `[[ActiveQueryInterface::via()]]`.) + */ + public function link($name, $model, $extraColumns = []); - /** - * Destroys the relationship between two records. - * - * The record with the foreign key of the relationship will be deleted if `$delete` is true. - * Otherwise, the foreign key will be set null and the record will be saved without validation. - * - * @param string $name the case sensitive name of the relationship. - * @param static $model the model to be unlinked from the current one. - * @param boolean $delete whether to delete the model that contains the foreign key. - * If false, the model's foreign key will be set null and saved. - * If true, the model containing the foreign key will be deleted. - */ - public function unlink($name, $model, $delete = false); + /** + * Destroys the relationship between two records. + * + * The record with the foreign key of the relationship will be deleted if `$delete` is true. + * Otherwise, the foreign key will be set null and the record will be saved without validation. + * + * @param string $name the case sensitive name of the relationship. + * @param static $model the model to be unlinked from the current one. + * @param boolean $delete whether to delete the model that contains the foreign key. + * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. + */ + public function unlink($name, $model, $delete = false); } diff --git a/framework/db/ActiveRelationTrait.php b/framework/db/ActiveRelationTrait.php index 860be0597cb..6676165e234 100644 --- a/framework/db/ActiveRelationTrait.php +++ b/framework/db/ActiveRelationTrait.php @@ -19,403 +19,410 @@ */ trait ActiveRelationTrait { - /** - * @var boolean whether this query represents a relation to more than one record. - * This property is only used in relational context. If true, this relation will - * populate all query results into AR instances using [[all()]]. - * If false, only the first row of the results will be retrieved using [[one()]]. - */ - public $multiple; - /** - * @var ActiveRecord the primary model of a relational query. - * This is used only in lazy loading with dynamic query options. - */ - public $primaryModel; - /** - * @var array the columns of the primary and foreign tables that establish a relation. - * The array keys must be columns of the table for this relation, and the array values - * must be the corresponding columns from the primary table. - * Do not prefix or quote the column names as this will be done automatically by Yii. - * This property is only used in relational context. - */ - public $link; - /** - * @var array the query associated with the pivot table. Please call [[via()]] - * to set this property instead of directly setting it. - * This property is only used in relational context. - * @see via() - */ - public $via; - /** - * @var string the name of the relation that is the inverse of this relation. - * For example, an order has a customer, which means the inverse of the "customer" relation - * is the "orders", and the inverse of the "orders" relation is the "customer". - * If this property is set, the primary record(s) will be referenced through the specified relation. - * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, - * and accessing the customer of an order will not trigger new DB query. - * This property is only used in relational context. - * @see inverseOf() - */ - public $inverseOf; - - - /** - * Clones internal objects. - */ - public function __clone() - { - parent::__clone(); - // make a clone of "via" object so that the same query object can be reused multiple times - if (is_object($this->via)) { - $this->via = clone $this->via; - } elseif (is_array($this->via)) { - $this->via = [$this->via[0], clone $this->via[1]]; - } - } - - /** - * Specifies the relation associated with the pivot table. - * - * Use this method to specify a pivot record/table when declaring a relation in the [[ActiveRecord]] class: - * - * ```php - * public function getOrders() - * { - * return $this->hasOne(Order::className(), ['id' => 'order_id']); - * } - * - * public function getOrderItems() - * { - * return $this->hasMany(Item::className(), ['id' => 'item_id']) - * ->via('orders', ['order_id' => 'id']); - * } - * ``` - * - * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. - * Its signature should be `function($query)`, where `$query` is the query to be customized. - * @return static the relation object itself. - */ - public function via($relationName, $callable = null) - { - $relation = $this->primaryModel->getRelation($relationName); - $this->via = [$relationName, $relation]; - if ($callable !== null) { - call_user_func($callable, $relation); - } - return $this; - } - - /** - * Sets the name of the relation that is the inverse of this relation. - * For example, an order has a customer, which means the inverse of the "customer" relation - * is the "orders", and the inverse of the "orders" relation is the "customer". - * If this property is set, the primary record(s) will be referenced through the specified relation. - * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, - * and accessing the customer of an order will not trigger a new DB query. - * - * Use this method when declaring a relation in the [[ActiveRecord]] class: - * - * ```php - * public function getOrders() - * { - * return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer'); - * } - * ``` - * - * @param string $relationName the name of the relation that is the inverse of this relation. - * @return static the relation object itself. - */ - public function inverseOf($relationName) - { - $this->inverseOf = $relationName; - return $this; - } - - /** - * Finds the related records for the specified primary record. - * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion. - * @param string $name the relation name - * @param ActiveRecordInterface|BaseActiveRecord $model the primary model - * @return mixed the related record(s) - * @throws InvalidParamException if the relation is invalid - */ - public function findFor($name, $model) - { - if (method_exists($model, 'get' . $name)) { - $method = new \ReflectionMethod($model, 'get' . $name); - $realName = lcfirst(substr($method->getName(), 3)); - if ($realName !== $name) { - throw new InvalidParamException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\"."); - } - } - - $related = $this->multiple ? $this->all() : $this->one(); - - if ($this->inverseOf === null || empty($related)) { - return $related; - } - - $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf); - - if ($this->multiple) { - foreach ($related as $i => $relatedModel) { - if ($relatedModel instanceof ActiveRecordInterface) { - $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model); - } else { - $related[$i][$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model; - } - } - } else { - if ($related instanceof ActiveRecordInterface) { - $related->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model); - } else { - $related[$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model; - } - } - - return $related; - } - - /** - * Finds the related records and populates them into the primary models. - * @param string $name the relation name - * @param array $primaryModels primary models - * @return array the related models - * @throws InvalidConfigException if [[link]] is invalid - */ - public function populateRelation($name, &$primaryModels) - { - if (!is_array($this->link)) { - throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); - } - - if ($this->via instanceof self) { - // via pivot table - /** @var ActiveRelationTrait $viaQuery */ - $viaQuery = $this->via; - $viaModels = $viaQuery->findPivotRows($primaryModels); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var ActiveRelationTrait $viaQuery */ - list($viaName, $viaQuery) = $this->via; - $viaQuery->primaryModel = null; - $viaModels = $viaQuery->populateRelation($viaName, $primaryModels); - $this->filterByModels($viaModels); - } else { - $this->filterByModels($primaryModels); - } - - if (count($primaryModels) === 1 && !$this->multiple) { - $model = $this->one(); - foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModel instanceof ActiveRecordInterface) { - $primaryModel->populateRelation($name, $model); - } else { - $primaryModels[$i][$name] = $model; - } - if ($this->inverseOf !== null) { - $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf); - } - } - return [$model]; - } else { - $models = $this->all(); - if (isset($viaModels, $viaQuery)) { - $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); - } else { - $buckets = $this->buildBuckets($models, $this->link); - } - - $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); - foreach ($primaryModels as $i => $primaryModel) { - $key = $this->getModelKey($primaryModel, $link); - $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); - if ($primaryModel instanceof ActiveRecordInterface) { - $primaryModel->populateRelation($name, $value); - } else { - $primaryModels[$i][$name] = $value; - } - } - if ($this->inverseOf !== null) { - $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf); - } - return $models; - } - } - - private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name) - { - if (empty($models) || empty($primaryModels)) { - return; - } - $model = reset($models); - $relation = $model instanceof ActiveRecordInterface ? $model->getRelation($name) : (new $this->modelClass)->getRelation($name); - - if ($relation->multiple) { - $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false); - if ($model instanceof ActiveRecordInterface) { - foreach ($models as $model) { - $key = $this->getModelKey($model, $relation->link); - $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []); - } - } else { - foreach ($primaryModels as $i => $primaryModel) { - if ($this->multiple) { - foreach ($primaryModel as $j => $m) { - $key = $this->getModelKey($m, $relation->link); - $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : []; - } - } elseif (!empty($primaryModel[$primaryName])) { - $key = $this->getModelKey($primaryModel[$primaryName], $relation->link); - $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : []; - } - } - } - } else { - if ($this->multiple) { - foreach ($primaryModels as $i => $primaryModel) { - foreach ($primaryModel[$primaryName] as $j => $m) { - if ($m instanceof ActiveRecordInterface) { - $m->populateRelation($name, $primaryModel); - } else { - $primaryModels[$i][$primaryName][$j][$name] = $primaryModel; - } - } - } - } else { - foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) { - $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel); - } elseif (!empty($primaryModels[$i][$primaryName])) { - $primaryModels[$i][$primaryName][$name] = $primaryModel; - } - } - } - } - } - - /** - * @param array $models - * @param array $link - * @param array $viaModels - * @param array $viaLink - * @param boolean $checkMultiple - * @return array - */ - private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true) - { - if ($viaModels !== null) { - $map = []; - $viaLinkKeys = array_keys($viaLink); - $linkValues = array_values($link); - foreach ($viaModels as $viaModel) { - $key1 = $this->getModelKey($viaModel, $viaLinkKeys); - $key2 = $this->getModelKey($viaModel, $linkValues); - $map[$key2][$key1] = true; - } - } - - $buckets = []; - $linkKeys = array_keys($link); - - if (isset($map)) { - foreach ($models as $i => $model) { - $key = $this->getModelKey($model, $linkKeys); - if (isset($map[$key])) { - foreach (array_keys($map[$key]) as $key2) { - if ($this->indexBy !== null) { - $buckets[$key2][$i] = $model; - } else { - $buckets[$key2][] = $model; - } - } - } - } - } else { - foreach ($models as $i => $model) { - $key = $this->getModelKey($model, $linkKeys); - if ($this->indexBy !== null) { - $buckets[$key][$i] = $model; - } else { - $buckets[$key][] = $model; - } - } - } - - if ($checkMultiple && !$this->multiple) { - foreach ($buckets as $i => $bucket) { - $buckets[$i] = reset($bucket); - } - } - return $buckets; - } - - /** - * @param array $models - */ - private function filterByModels($models) - { - $attributes = array_keys($this->link); - $values = []; - if (count($attributes) === 1) { - // single key - $attribute = reset($this->link); - foreach ($models as $model) { - if (($value = $model[$attribute]) !== null) { - $values[] = $value; - } - } - } else { - // composite keys - foreach ($models as $model) { - $v = []; - foreach ($this->link as $attribute => $link) { - $v[$attribute] = $model[$link]; - } - $values[] = $v; - } - } - $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); - } - - /** - * @param ActiveRecord|array $model - * @param array $attributes - * @return string - */ - private function getModelKey($model, $attributes) - { - if (count($attributes) > 1) { - $key = []; - foreach ($attributes as $attribute) { - $key[] = $model[$attribute]; - } - return serialize($key); - } else { - $attribute = reset($attributes); - $key = $model[$attribute]; - return is_scalar($key) ? $key : serialize($key); - } - } - - /** - * @param array $primaryModels either array of AR instances or arrays - * @return array - */ - private function findPivotRows($primaryModels) - { - if (empty($primaryModels)) { - return []; - } - $this->filterByModels($primaryModels); - /** @var ActiveRecord $primaryModel */ - $primaryModel = reset($primaryModels); - if (!$primaryModel instanceof ActiveRecordInterface) { - // when primaryModels are array of arrays (asArray case) - $primaryModel = new $this->modelClass; - } - return $this->asArray()->all($primaryModel->getDb()); - } + /** + * @var boolean whether this query represents a relation to more than one record. + * This property is only used in relational context. If true, this relation will + * populate all query results into AR instances using [[all()]]. + * If false, only the first row of the results will be retrieved using [[one()]]. + */ + public $multiple; + /** + * @var ActiveRecord the primary model of a relational query. + * This is used only in lazy loading with dynamic query options. + */ + public $primaryModel; + /** + * @var array the columns of the primary and foreign tables that establish a relation. + * The array keys must be columns of the table for this relation, and the array values + * must be the corresponding columns from the primary table. + * Do not prefix or quote the column names as this will be done automatically by Yii. + * This property is only used in relational context. + */ + public $link; + /** + * @var array the query associated with the pivot table. Please call [[via()]] + * to set this property instead of directly setting it. + * This property is only used in relational context. + * @see via() + */ + public $via; + /** + * @var string the name of the relation that is the inverse of this relation. + * For example, an order has a customer, which means the inverse of the "customer" relation + * is the "orders", and the inverse of the "orders" relation is the "customer". + * If this property is set, the primary record(s) will be referenced through the specified relation. + * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, + * and accessing the customer of an order will not trigger new DB query. + * This property is only used in relational context. + * @see inverseOf() + */ + public $inverseOf; + + /** + * Clones internal objects. + */ + public function __clone() + { + parent::__clone(); + // make a clone of "via" object so that the same query object can be reused multiple times + if (is_object($this->via)) { + $this->via = clone $this->via; + } elseif (is_array($this->via)) { + $this->via = [$this->via[0], clone $this->via[1]]; + } + } + + /** + * Specifies the relation associated with the pivot table. + * + * Use this method to specify a pivot record/table when declaring a relation in the [[ActiveRecord]] class: + * + * ```php + * public function getOrders() + * { + * return $this->hasOne(Order::className(), ['id' => 'order_id']); + * } + * + * public function getOrderItems() + * { + * return $this->hasMany(Item::className(), ['id' => 'item_id']) + * ->via('orders', ['order_id' => 'id']); + * } + * ``` + * + * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return static the relation object itself. + */ + public function via($relationName, $callable = null) + { + $relation = $this->primaryModel->getRelation($relationName); + $this->via = [$relationName, $relation]; + if ($callable !== null) { + call_user_func($callable, $relation); + } + + return $this; + } + + /** + * Sets the name of the relation that is the inverse of this relation. + * For example, an order has a customer, which means the inverse of the "customer" relation + * is the "orders", and the inverse of the "orders" relation is the "customer". + * If this property is set, the primary record(s) will be referenced through the specified relation. + * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, + * and accessing the customer of an order will not trigger a new DB query. + * + * Use this method when declaring a relation in the [[ActiveRecord]] class: + * + * ```php + * public function getOrders() + * { + * return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer'); + * } + * ``` + * + * @param string $relationName the name of the relation that is the inverse of this relation. + * @return static the relation object itself. + */ + public function inverseOf($relationName) + { + $this->inverseOf = $relationName; + + return $this; + } + + /** + * Finds the related records for the specified primary record. + * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion. + * @param string $name the relation name + * @param ActiveRecordInterface|BaseActiveRecord $model the primary model + * @return mixed the related record(s) + * @throws InvalidParamException if the relation is invalid + */ + public function findFor($name, $model) + { + if (method_exists($model, 'get' . $name)) { + $method = new \ReflectionMethod($model, 'get' . $name); + $realName = lcfirst(substr($method->getName(), 3)); + if ($realName !== $name) { + throw new InvalidParamException('Relation names are case sensitive. ' . get_class($model) . " has a relation named \"$realName\" instead of \"$name\"."); + } + } + + $related = $this->multiple ? $this->all() : $this->one(); + + if ($this->inverseOf === null || empty($related)) { + return $related; + } + + $inverseRelation = (new $this->modelClass)->getRelation($this->inverseOf); + + if ($this->multiple) { + foreach ($related as $i => $relatedModel) { + if ($relatedModel instanceof ActiveRecordInterface) { + $relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model); + } else { + $related[$i][$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model; + } + } + } else { + if ($related instanceof ActiveRecordInterface) { + $related->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$model] : $model); + } else { + $related[$this->inverseOf] = $inverseRelation->multiple ? [$model] : $model; + } + } + + return $related; + } + + /** + * Finds the related records and populates them into the primary models. + * @param string $name the relation name + * @param array $primaryModels primary models + * @return array the related models + * @throws InvalidConfigException if [[link]] is invalid + */ + public function populateRelation($name, &$primaryModels) + { + if (!is_array($this->link)) { + throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); + } + + if ($this->via instanceof self) { + // via pivot table + /** @var ActiveRelationTrait $viaQuery */ + $viaQuery = $this->via; + $viaModels = $viaQuery->findPivotRows($primaryModels); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var ActiveRelationTrait $viaQuery */ + list($viaName, $viaQuery) = $this->via; + $viaQuery->primaryModel = null; + $viaModels = $viaQuery->populateRelation($viaName, $primaryModels); + $this->filterByModels($viaModels); + } else { + $this->filterByModels($primaryModels); + } + + if (count($primaryModels) === 1 && !$this->multiple) { + $model = $this->one(); + foreach ($primaryModels as $i => $primaryModel) { + if ($primaryModel instanceof ActiveRecordInterface) { + $primaryModel->populateRelation($name, $model); + } else { + $primaryModels[$i][$name] = $model; + } + if ($this->inverseOf !== null) { + $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf); + } + } + + return [$model]; + } else { + $models = $this->all(); + if (isset($viaModels, $viaQuery)) { + $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); + } else { + $buckets = $this->buildBuckets($models, $this->link); + } + + $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); + foreach ($primaryModels as $i => $primaryModel) { + $key = $this->getModelKey($primaryModel, $link); + $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); + if ($primaryModel instanceof ActiveRecordInterface) { + $primaryModel->populateRelation($name, $value); + } else { + $primaryModels[$i][$name] = $value; + } + } + if ($this->inverseOf !== null) { + $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf); + } + + return $models; + } + } + + private function populateInverseRelation(&$primaryModels, $models, $primaryName, $name) + { + if (empty($models) || empty($primaryModels)) { + return; + } + $model = reset($models); + $relation = $model instanceof ActiveRecordInterface ? $model->getRelation($name) : (new $this->modelClass)->getRelation($name); + + if ($relation->multiple) { + $buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false); + if ($model instanceof ActiveRecordInterface) { + foreach ($models as $model) { + $key = $this->getModelKey($model, $relation->link); + $model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []); + } + } else { + foreach ($primaryModels as $i => $primaryModel) { + if ($this->multiple) { + foreach ($primaryModel as $j => $m) { + $key = $this->getModelKey($m, $relation->link); + $primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : []; + } + } elseif (!empty($primaryModel[$primaryName])) { + $key = $this->getModelKey($primaryModel[$primaryName], $relation->link); + $primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : []; + } + } + } + } else { + if ($this->multiple) { + foreach ($primaryModels as $i => $primaryModel) { + foreach ($primaryModel[$primaryName] as $j => $m) { + if ($m instanceof ActiveRecordInterface) { + $m->populateRelation($name, $primaryModel); + } else { + $primaryModels[$i][$primaryName][$j][$name] = $primaryModel; + } + } + } + } else { + foreach ($primaryModels as $i => $primaryModel) { + if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) { + $primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel); + } elseif (!empty($primaryModels[$i][$primaryName])) { + $primaryModels[$i][$primaryName][$name] = $primaryModel; + } + } + } + } + } + + /** + * @param array $models + * @param array $link + * @param array $viaModels + * @param array $viaLink + * @param boolean $checkMultiple + * @return array + */ + private function buildBuckets($models, $link, $viaModels = null, $viaLink = null, $checkMultiple = true) + { + if ($viaModels !== null) { + $map = []; + $viaLinkKeys = array_keys($viaLink); + $linkValues = array_values($link); + foreach ($viaModels as $viaModel) { + $key1 = $this->getModelKey($viaModel, $viaLinkKeys); + $key2 = $this->getModelKey($viaModel, $linkValues); + $map[$key2][$key1] = true; + } + } + + $buckets = []; + $linkKeys = array_keys($link); + + if (isset($map)) { + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if (isset($map[$key])) { + foreach (array_keys($map[$key]) as $key2) { + if ($this->indexBy !== null) { + $buckets[$key2][$i] = $model; + } else { + $buckets[$key2][] = $model; + } + } + } + } + } else { + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if ($this->indexBy !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } + } + + if ($checkMultiple && !$this->multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + + return $buckets; + } + + /** + * @param array $models + */ + private function filterByModels($models) + { + $attributes = array_keys($this->link); + $values = []; + if (count($attributes) === 1) { + // single key + $attribute = reset($this->link); + foreach ($models as $model) { + if (($value = $model[$attribute]) !== null) { + $values[] = $value; + } + } + } else { + // composite keys + foreach ($models as $model) { + $v = []; + foreach ($this->link as $attribute => $link) { + $v[$attribute] = $model[$link]; + } + $values[] = $v; + } + } + $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); + } + + /** + * @param ActiveRecord|array $model + * @param array $attributes + * @return string + */ + private function getModelKey($model, $attributes) + { + if (count($attributes) > 1) { + $key = []; + foreach ($attributes as $attribute) { + $key[] = $model[$attribute]; + } + + return serialize($key); + } else { + $attribute = reset($attributes); + $key = $model[$attribute]; + + return is_scalar($key) ? $key : serialize($key); + } + } + + /** + * @param array $primaryModels either array of AR instances or arrays + * @return array + */ + private function findPivotRows($primaryModels) + { + if (empty($primaryModels)) { + return []; + } + $this->filterByModels($primaryModels); + /** @var ActiveRecord $primaryModel */ + $primaryModel = reset($primaryModels); + if (!$primaryModel instanceof ActiveRecordInterface) { + // when primaryModels are array of arrays (asArray case) + $primaryModel = new $this->modelClass; + } + + return $this->asArray()->all($primaryModel->getDb()); + } } diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index a1dd13e3179..b781a7ed104 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -40,1360 +40,1377 @@ */ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface { - /** - * @event Event an event that is triggered when the record is initialized via [[init()]]. - */ - const EVENT_INIT = 'init'; - /** - * @event Event an event that is triggered after the record is created and populated with query result. - */ - const EVENT_AFTER_FIND = 'afterFind'; - /** - * @event ModelEvent an event that is triggered before inserting a record. - * You may set [[ModelEvent::isValid]] to be false to stop the insertion. - */ - const EVENT_BEFORE_INSERT = 'beforeInsert'; - /** - * @event Event an event that is triggered after a record is inserted. - */ - const EVENT_AFTER_INSERT = 'afterInsert'; - /** - * @event ModelEvent an event that is triggered before updating a record. - * You may set [[ModelEvent::isValid]] to be false to stop the update. - */ - const EVENT_BEFORE_UPDATE = 'beforeUpdate'; - /** - * @event Event an event that is triggered after a record is updated. - */ - const EVENT_AFTER_UPDATE = 'afterUpdate'; - /** - * @event ModelEvent an event that is triggered before deleting a record. - * You may set [[ModelEvent::isValid]] to be false to stop the deletion. - */ - const EVENT_BEFORE_DELETE = 'beforeDelete'; - /** - * @event Event an event that is triggered after a record is deleted. - */ - const EVENT_AFTER_DELETE = 'afterDelete'; - - /** - * @var array attribute values indexed by attribute names - */ - private $_attributes = []; - /** - * @var array old attribute values indexed by attribute names. - */ - private $_oldAttributes; - /** - * @var array related models indexed by the relation names - */ - private $_related = []; - - - /** - * Creates an [[ActiveQuery]] instance for query purpose. - * - * The returned [[ActiveQuery]] instance can be further customized by calling - * methods defined in [[ActiveQuery]] before `one()`, `all()` or `value()` is - * called to return the populated active records: - * - * ~~~ - * // find all customers - * $customers = Customer::find()->all(); - * - * // find all active customers and order them by their age: - * $customers = Customer::find() - * ->where(['status' => 1]) - * ->orderBy('age') - * ->all(); - * - * // find a single customer whose primary key value is 10 - * $customer = Customer::find(10); - * - * // the above is equivalent to: - * $customer = Customer::find()->where(['id' => 10])->one(); - * - * // find a single customer whose age is 30 and whose status is 1 - * $customer = Customer::find(['age' => 30, 'status' => 1]); - * - * // the above is equivalent to: - * $customer = Customer::find()->where(['age' => 30, 'status' => 1])->one(); - * ~~~ - * - * @param mixed $q the query parameter. This can be one of the followings: - * - * - a scalar value (integer or string): query by a single primary key value and return the - * corresponding record. - * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. - * - null: return a new [[ActiveQuery]] object for further query purpose. - * - * @return ActiveQuery|static|null When `$q` is null, a new [[ActiveQuery]] instance - * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be - * returned (null will be returned if there is no matching). - * @throws InvalidConfigException if the AR class does not have a primary key - * @see createQuery() - */ - public static function find($q = null) - { - $query = static::createQuery(); - if (is_array($q)) { - return $query->andWhere($q)->one(); - } elseif ($q !== null) { - // query by primary key - $primaryKey = static::primaryKey(); - if (isset($primaryKey[0])) { - return $query->andWhere([$primaryKey[0] => $q])->one(); - } else { - throw new InvalidConfigException(get_called_class() . ' must have a primary key.'); - } - } - return $query; - } - - /** - * Updates the whole table using the provided attribute values and conditions. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], 'status = 2'); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = '') - { - throw new NotSupportedException(__METHOD__ . ' is not supported.'); - } - - /** - * Updates the whole table using the provided counter changes and conditions. - * For example, to increment all customers' age by 1, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @return integer the number of rows updated - */ - public static function updateAllCounters($counters, $condition = '') - { - throw new NotSupportedException(__METHOD__ . ' is not supported.'); - } - - /** - * Deletes rows in the table using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll('status = 3'); - * ~~~ - * - * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. - * Please refer to [[Query::where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = '', $params = []) - { - throw new NotSupportedException(__METHOD__ . ' is not supported.'); - } - - /** - * Returns the name of the column that stores the lock version for implementing optimistic locking. - * - * Optimistic locking allows multiple users to access the same record for edits and avoids - * potential conflicts. In case when a user attempts to save the record upon some staled data - * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, - * and the update or deletion is skipped. - * - * Optimistic locking is only supported by [[update()]] and [[delete()]]. - * - * To use Optimistic locking: - * - * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. - * Override this method to return the name of this column. - * 2. In the Web form that collects the user input, add a hidden field that stores - * the lock version of the recording being updated. - * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] - * and implement necessary business logic (e.g. merging the changes, prompting stated data) - * to resolve the conflict. - * - * @return string the column name that stores the lock version of a table row. - * If null is returned (default implemented), optimistic locking will not be supported. - */ - public function optimisticLock() - { - return null; - } - - /** - * PHP getter magic method. - * This method is overridden so that attributes and related objects can be accessed like properties. - * - * @param string $name property name - * @throws \yii\base\InvalidParamException if relation name is wrong - * @return mixed property value - * @see getAttribute() - */ - public function __get($name) - { - if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { - return $this->_attributes[$name]; - } elseif ($this->hasAttribute($name)) { - return null; - } else { - if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) { - return $this->_related[$name]; - } - $value = parent::__get($name); - if ($value instanceof ActiveQueryInterface) { - return $this->_related[$name] = $value->findFor($name, $this); - } else { - return $value; - } - } - } - - /** - * PHP setter magic method. - * This method is overridden so that AR attributes can be accessed like properties. - * @param string $name property name - * @param mixed $value property value - */ - public function __set($name, $value) - { - if ($this->hasAttribute($name)) { - $this->_attributes[$name] = $value; - } else { - parent::__set($name, $value); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking if the named attribute is null or not. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - try { - return $this->__get($name) !== null; - } catch (\Exception $e) { - return false; - } - } - - /** - * Sets a component property to be null. - * This method overrides the parent implementation by clearing - * the specified attribute value. - * @param string $name the property name or the event name - */ - public function __unset($name) - { - if ($this->hasAttribute($name)) { - unset($this->_attributes[$name]); - } elseif (array_key_exists($name, $this->_related)) { - unset($this->_related[$name]); - } elseif ($this->getRelation($name, false) === null) { - parent::__unset($name); - } - } - - /** - * Declares a `has-one` relation. - * The declaration is returned in terms of a relational [[ActiveQuery]] instance - * through which the related record can be queried and retrieved back. - * - * A `has-one` relation means that there is at most one related record matching - * the criteria set by this relation, e.g., a customer has one country. - * - * For example, to declare the `country` relation for `Customer` class, we can write - * the following code in the `Customer` class: - * - * ~~~ - * public function getCountry() - * { - * return $this->hasOne(Country::className(), ['id' => 'country_id']); - * } - * ~~~ - * - * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name - * in the related class `Country`, while the 'country_id' value refers to an attribute name - * in the current AR class. - * - * Call methods declared in [[ActiveQuery]] to further customize the relation. - * - * @param string $class the class name of the related record - * @param array $link the primary-foreign key constraint. The keys of the array refer to - * the attributes of the record associated with the `$class` model, while the values of the - * array refer to the corresponding attributes in **this** AR class. - * @return ActiveQueryInterface the relational query object. - */ - public function hasOne($class, $link) - { - /** @var ActiveRecordInterface $class */ - return $class::createQuery([ - 'modelClass' => $class, - 'primaryModel' => $this, - 'link' => $link, - 'multiple' => false, - ]); - } - - /** - * Declares a `has-many` relation. - * The declaration is returned in terms of a relational [[ActiveQuery]] instance - * through which the related record can be queried and retrieved back. - * - * A `has-many` relation means that there are multiple related records matching - * the criteria set by this relation, e.g., a customer has many orders. - * - * For example, to declare the `orders` relation for `Customer` class, we can write - * the following code in the `Customer` class: - * - * ~~~ - * public function getOrders() - * { - * return $this->hasMany(Order::className(), ['customer_id' => 'id']); - * } - * ~~~ - * - * Note that in the above, the 'customer_id' key in the `$link` parameter refers to - * an attribute name in the related class `Order`, while the 'id' value refers to - * an attribute name in the current AR class. - * - * Call methods declared in [[ActiveQuery]] to further customize the relation. - * - * @param string $class the class name of the related record - * @param array $link the primary-foreign key constraint. The keys of the array refer to - * the attributes of the record associated with the `$class` model, while the values of the - * array refer to the corresponding attributes in **this** AR class. - * @return ActiveQueryInterface the relational query object. - */ - public function hasMany($class, $link) - { - /** @var ActiveRecordInterface $class */ - return $class::createQuery([ - 'modelClass' => $class, - 'primaryModel' => $this, - 'link' => $link, - 'multiple' => true, - ]); - } - - /** - * Populates the named relation with the related records. - * Note that this method does not check if the relation exists or not. - * @param string $name the relation name (case-sensitive) - * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation. - */ - public function populateRelation($name, $records) - { - $this->_related[$name] = $records; - } - - /** - * Check whether the named relation has been populated with records. - * @param string $name the relation name (case-sensitive) - * @return boolean whether relation has been populated with records. - */ - public function isRelationPopulated($name) - { - return array_key_exists($name, $this->_related); - } - - /** - * Returns all populated related records. - * @return array an array of related records indexed by relation names. - */ - public function getRelatedRecords() - { - return $this->_related; - } - - /** - * Returns a value indicating whether the model has an attribute with the specified name. - * @param string $name the name of the attribute - * @return boolean whether the model has an attribute with the specified name. - */ - public function hasAttribute($name) - { - return isset($this->_attributes[$name]) || in_array($name, $this->attributes()); - } - - /** - * Returns the named attribute value. - * If this record is the result of a query and the attribute is not loaded, - * null will be returned. - * @param string $name the attribute name - * @return mixed the attribute value. Null if the attribute is not set or does not exist. - * @see hasAttribute() - */ - public function getAttribute($name) - { - return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; - } - - /** - * Sets the named attribute value. - * @param string $name the attribute name - * @param mixed $value the attribute value. - * @throws InvalidParamException if the named attribute does not exist. - * @see hasAttribute() - */ - public function setAttribute($name, $value) - { - if ($this->hasAttribute($name)) { - $this->_attributes[$name] = $value; - } else { - throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); - } - } - - /** - * Returns the old attribute values. - * @return array the old attribute values (name-value pairs) - */ - public function getOldAttributes() - { - return $this->_oldAttributes === null ? [] : $this->_oldAttributes; - } - - /** - * Sets the old attribute values. - * All existing old attribute values will be discarded. - * @param array $values old attribute values to be set. - */ - public function setOldAttributes($values) - { - $this->_oldAttributes = $values; - } - - /** - * Returns the old value of the named attribute. - * If this record is the result of a query and the attribute is not loaded, - * null will be returned. - * @param string $name the attribute name - * @return mixed the old attribute value. Null if the attribute is not loaded before - * or does not exist. - * @see hasAttribute() - */ - public function getOldAttribute($name) - { - return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; - } - - /** - * Sets the old value of the named attribute. - * @param string $name the attribute name - * @param mixed $value the old attribute value. - * @throws InvalidParamException if the named attribute does not exist. - * @see hasAttribute() - */ - public function setOldAttribute($name, $value) - { - if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) { - $this->_oldAttributes[$name] = $value; - } else { - throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); - } - } - - /** - * Marks an attribute dirty. - * This method may be called to force updating a record when calling [[update()]], - * even if there is no change being made to the record. - * @param string $name the attribute name - */ - public function markAttributeDirty($name) - { - unset($this->_oldAttributes[$name]); - } - - /** - * Returns a value indicating whether the named attribute has been changed. - * @param string $name the name of the attribute - * @return boolean whether the attribute has been changed - */ - public function isAttributeChanged($name) - { - if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { - return $this->_attributes[$name] !== $this->_oldAttributes[$name]; - } else { - return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]); - } - } - - /** - * Returns the attribute values that have been modified since they are loaded or saved most recently. - * @param string[]|null $names the names of the attributes whose values may be returned if they are - * changed recently. If null, [[attributes()]] will be used. - * @return array the changed attribute values (name-value pairs) - */ - public function getDirtyAttributes($names = null) - { - if ($names === null) { - $names = $this->attributes(); - } - $names = array_flip($names); - $attributes = []; - if ($this->_oldAttributes === null) { - foreach ($this->_attributes as $name => $value) { - if (isset($names[$name])) { - $attributes[$name] = $value; - } - } - } else { - foreach ($this->_attributes as $name => $value) { - if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { - $attributes[$name] = $value; - } - } - } - return $attributes; - } - - /** - * Saves the current record. - * - * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] - * when [[isNewRecord]] is false. - * - * For example, to save a customer record: - * - * ~~~ - * $customer = new Customer; // or $customer = Customer::find($id); - * $customer->name = $name; - * $customer->email = $email; - * $customer->save(); - * ~~~ - * - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be saved to database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the saving succeeds - */ - public function save($runValidation = true, $attributes = null) - { - if ($this->getIsNewRecord()) { - return $this->insert($runValidation, $attributes); - } else { - return $this->update($runValidation, $attributes) !== false; - } - } - - /** - * Saves the changes to this active record into the associated database table. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. save the record into database. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[dirtyAttributes|changed attribute values]] will be saved into database. - * - * For example, to update a customer record: - * - * ~~~ - * $customer = Customer::find($id); - * $customer->name = $name; - * $customer->email = $email; - * $customer->update(); - * ~~~ - * - * Note that it is possible the update does not affect any row in the table. - * In this case, this method will return 0. For this reason, you should use the following - * code to check if update() is successful or not: - * - * ~~~ - * if ($this->update() !== false) { - * // update successful - * } else { - * // update failed - * } - * ~~~ - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return integer|boolean the number of rows affected, or false if validation fails - * or [[beforeSave()]] stops the updating process. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being updated is outdated. - * @throws \Exception in case update failed. - */ - public function update($runValidation = true, $attributes = null) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - return $this->updateInternal($attributes); - } - - /** - * Updates the specified attributes. - * - * This method is a shortcut to [[update()]] when data validation is not needed - * and only a list of attributes need to be updated. - * - * You may specify the attributes to be updated as name list or name-value pairs. - * If the latter, the corresponding attribute values will be modified accordingly. - * The method will then save the specified attributes into database. - * - * Note that this method will NOT perform data validation. - * - * @param array $attributes the attributes (names or name-value pairs) to be updated - * @return integer|boolean the number of rows affected, or false if [[beforeSave()]] stops the updating process. - */ - public function updateAttributes($attributes) - { - $attrs = []; - foreach ($attributes as $name => $value) { - if (is_integer($name)) { - $attrs[] = $value; - } else { - $this->$name = $value; - $attrs[] = $name; - } - } - return $this->update(false, $attrs); - } - - /** - * @see update() - * @throws StaleObjectException - */ - protected function updateInternal($attributes = null) - { - if (!$this->beforeSave(false)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $this->afterSave(false); - return 0; - } - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - if (!isset($values[$lock])) { - $values[$lock] = $this->$lock + 1; - } - $condition[$lock] = $this->$lock; - } - // We do not check the return value of updateAll() because it's possible - // that the UPDATE statement doesn't change anything and thus returns 0. - $rows = $this->updateAll($values, $condition); - - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); - } - - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - $this->afterSave(false); - return $rows; - } - - /** - * Updates one or several counter columns for the current AR object. - * Note that this method differs from [[updateAllCounters()]] in that it only - * saves counters for the current AR object. - * - * An example usage is as follows: - * - * ~~~ - * $post = Post::find($id); - * $post->updateCounters(['view_count' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value) - * Use negative values if you want to decrement the counters. - * @return boolean whether the saving is successful - * @see updateAllCounters() - */ - public function updateCounters($counters) - { - if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) { - foreach ($counters as $name => $value) { - $this->_attributes[$name] += $value; - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - return true; - } else { - return false; - } - } - - /** - * Deletes the table row corresponding to this active record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeDelete()]]. If the method returns false, it will skip the - * rest of the steps; - * 2. delete the record from the database; - * 3. call [[afterDelete()]]. - * - * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] - * will be raised by the corresponding methods. - * - * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. - * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data - * being deleted is outdated. - * @throws \Exception in case delete failed. - */ - public function delete() - { - $result = false; - if ($this->beforeDelete()) { - // we do not check the return value of deleteAll() because it's possible - // the record is already deleted in the database and thus the method will return 0 - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - $condition[$lock] = $this->$lock; - } - $result = $this->deleteAll($condition); - if ($lock !== null && !$result) { - throw new StaleObjectException('The object being deleted is outdated.'); - } - $this->_oldAttributes = null; - $this->afterDelete(); - } - return $result; - } - - /** - * Returns a value indicating whether the current record is new. - * @return boolean whether the record is new and should be inserted when calling [[save()]]. - */ - public function getIsNewRecord() - { - return $this->_oldAttributes === null; - } - - /** - * Sets the value indicating whether the record is new. - * @param boolean $value whether the record is new and should be inserted when calling [[save()]]. - * @see getIsNewRecord() - */ - public function setIsNewRecord($value) - { - $this->_oldAttributes = $value ? null : $this->_attributes; - } - - /** - * Initializes the object. - * This method is called at the end of the constructor. - * The default implementation will trigger an [[EVENT_INIT]] event. - * If you override this method, make sure you call the parent implementation at the end - * to ensure triggering of the event. - */ - public function init() - { - parent::init(); - $this->trigger(self::EVENT_INIT); - } - - /** - * This method is called when the AR object is created and populated with the query result. - * The default implementation will trigger an [[EVENT_AFTER_FIND]] event. - * When overriding this method, make sure you call the parent implementation to ensure the - * event is triggered. - */ - public function afterFind() - { - $this->trigger(self::EVENT_AFTER_FIND); - } - - /** - * This method is called at the beginning of inserting or updating a record. - * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true, - * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false. - * When overriding this method, make sure you call the parent implementation like the following: - * - * ~~~ - * public function beforeSave($insert) - * { - * if (parent::beforeSave($insert)) { - * // ...custom code here... - * return true; - * } else { - * return false; - * } - * } - * ~~~ - * - * @param boolean $insert whether this method called while inserting a record. - * If false, it means the method is called while updating a record. - * @return boolean whether the insertion or updating should continue. - * If false, the insertion or updating will be cancelled. - */ - public function beforeSave($insert) - { - $event = new ModelEvent; - $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); - return $event->isValid; - } - - /** - * This method is called at the end of inserting or updating a record. - * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true, - * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false. - * When overriding this method, make sure you call the parent implementation so that - * the event is triggered. - * @param boolean $insert whether this method called while inserting a record. - * If false, it means the method is called while updating a record. - */ - public function afterSave($insert) - { - $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE); - } - - /** - * This method is invoked before deleting a record. - * The default implementation raises the [[EVENT_BEFORE_DELETE]] event. - * When overriding this method, make sure you call the parent implementation like the following: - * - * ~~~ - * public function beforeDelete() - * { - * if (parent::beforeDelete()) { - * // ...custom code here... - * return true; - * } else { - * return false; - * } - * } - * ~~~ - * - * @return boolean whether the record should be deleted. Defaults to true. - */ - public function beforeDelete() - { - $event = new ModelEvent; - $this->trigger(self::EVENT_BEFORE_DELETE, $event); - return $event->isValid; - } - - /** - * This method is invoked after deleting a record. - * The default implementation raises the [[EVENT_AFTER_DELETE]] event. - * You may override this method to do postprocessing after the record is deleted. - * Make sure you call the parent implementation so that the event is raised properly. - */ - public function afterDelete() - { - $this->trigger(self::EVENT_AFTER_DELETE); - } - - /** - * Repopulates this active record with the latest data. - * @return boolean whether the row still exists in the database. If true, the latest data - * will be populated to this active record. Otherwise, this record will remain unchanged. - */ - public function refresh() - { - $record = $this->find($this->getPrimaryKey(true)); - if ($record === null) { - return false; - } - foreach ($this->attributes() as $name) { - $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null; - } - $this->_oldAttributes = $this->_attributes; - $this->_related = []; - return true; - } - - /** - * Returns a value indicating whether the given active record is the same as the current one. - * The comparison is made by comparing the table names and the primary key values of the two active records. - * If one of the records [[isNewRecord|is new]] they are also considered not equal. - * @param ActiveRecordInterface $record record to compare to - * @return boolean whether the two active records refer to the same row in the same database table. - */ - public function equals($record) - { - if ($this->getIsNewRecord() || $record->getIsNewRecord()) { - return false; - } - return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey(); - } - - /** - * Returns the primary key value(s). - * @param boolean $asArray whether to return the primary key value as an array. If true, - * the return value will be an array with column names as keys and column values as values. - * Note that for composite primary keys, an array will always be returned regardless of this parameter value. - * @property mixed The primary key value. An array (column name => column value) is returned if - * the primary key is composite. A string is returned otherwise (null will be returned if - * the key value is null). - * @return mixed the primary key value. An array (column name => column value) is returned if the primary key - * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if - * the key value is null). - */ - public function getPrimaryKey($asArray = false) - { - $keys = $this->primaryKey(); - if (count($keys) === 1 && !$asArray) { - return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; - } else { - $values = []; - foreach ($keys as $name) { - $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; - } - return $values; - } - } - - /** - * Returns the old primary key value(s). - * This refers to the primary key value that is populated into the record - * after executing a find method (e.g. find(), findAll()). - * The value remains unchanged even if the primary key attribute is manually assigned with a different value. - * @param boolean $asArray whether to return the primary key value as an array. If true, - * the return value will be an array with column name as key and column value as value. - * If this is false (default), a scalar value will be returned for non-composite primary key. - * @property mixed The old primary key value. An array (column name => column value) is - * returned if the primary key is composite. A string is returned otherwise (null will be - * returned if the key value is null). - * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key - * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if - * the key value is null). - */ - public function getOldPrimaryKey($asArray = false) - { - $keys = $this->primaryKey(); - if (count($keys) === 1 && !$asArray) { - return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; - } else { - $values = []; - foreach ($keys as $name) { - $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; - } - return $values; - } - } - - /** - * Populates an active record object using a row of data from the database/storage. - * - * This is an internal method meant to be called to create active record objects after - * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate - * the query results into active records. - * - * When calling this method manually you should call [[afterFind()]] on the created - * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]]. - * - * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance - * created by [[instantiate()]] beforehand. - * @param array $row attribute values (name => value) - */ - public static function populateRecord($record, $row) - { - $columns = array_flip($record->attributes()); - foreach ($row as $name => $value) { - if (isset($columns[$name])) { - $record->_attributes[$name] = $value; - } else { - $record->$name = $value; - } - } - $record->_oldAttributes = $record->_attributes; - } - - /** - * Creates an active record instance. - * - * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. - * It is not meant to be used for creating new records directly. - * - * You may override this method if the instance being created - * depends on the row data to be populated into the record. - * For example, by creating a record based on the value of a column, - * you may implement the so-called single-table inheritance mapping. - * @param array $row row data to be populated into the record. - * @return static the newly created active record - */ - public static function instantiate($row) - { - return new static; - } - - /** - * Returns whether there is an element at the specified offset. - * This method is required by the interface ArrayAccess. - * @param mixed $offset the offset to check on - * @return boolean whether there is an element at the specified offset. - */ - public function offsetExists($offset) - { - return $this->__isset($offset); - } - - /** - * Returns the relation object with the specified name. - * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object. - * It can be declared in either the Active Record class itself or one of its behaviors. - * @param string $name the relation name - * @param boolean $throwException whether to throw exception if the relation does not exist. - * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist - * and `$throwException` is false, null will be returned. - * @throws InvalidParamException if the named relation does not exist. - */ - public function getRelation($name, $throwException = true) - { - $getter = 'get' . $name; - try { - // the relation could be defined in a behavior - $relation = $this->$getter(); - } catch (UnknownMethodException $e) { - if ($throwException) { - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); - } else { - return null; - } - } - if (!$relation instanceof ActiveQueryInterface) { - if ($throwException) { - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); - } else { - return null; - } - } - - if (method_exists($this, $getter)) { - // relation name is case sensitive, trying to validate it when the relation is defined within this class - $method = new \ReflectionMethod($this, $getter); - $realName = lcfirst(substr($method->getName(), 3)); - if ($realName !== $name) { - if ($throwException) { - throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\"."); - } else { - return null; - } - } - } - - return $relation; - } - - /** - * Establishes the relationship between two models. - * - * The relationship is established by setting the foreign key value(s) in one model - * to be the corresponding primary key value(s) in the other model. - * The model with the foreign key will be saved into database without performing validation. - * - * If the relationship involves a pivot table, a new row will be inserted into the - * pivot table which contains the primary key values from both models. - * - * Note that this method requires that the primary key value is not null. - * - * @param string $name the case sensitive name of the relationship - * @param ActiveRecordInterface $model the model to be linked with the current one. - * @param array $extraColumns additional column values to be saved into the pivot table. - * This parameter is only meaningful for a relationship involving a pivot table - * (i.e., a relation set with [[ActiveRelationTrait::via()]] or `[[ActiveQuery::viaTable()]]`.) - * @throws InvalidCallException if the method is unable to link two models. - */ - public function link($name, $model, $extraColumns = []) - { - $relation = $this->getRelation($name); - - if ($relation->via !== null) { - if ($this->getIsNewRecord() || $model->getIsNewRecord()) { - throw new InvalidCallException('Unable to link models: both models must NOT be newly created.'); - } - if (is_array($relation->via)) { - /** @var ActiveQuery $viaRelation */ - list($viaName, $viaRelation) = $relation->via; - $viaClass = $viaRelation->modelClass; - // unset $viaName so that it can be reloaded to reflect the change - unset($this->_related[$viaName]); - } else { - $viaRelation = $relation->via; - $viaTable = reset($relation->via->from); - } - $columns = []; - foreach ($viaRelation->link as $a => $b) { - $columns[$a] = $this->$b; - } - foreach ($relation->link as $a => $b) { - $columns[$b] = $model->$a; - } - foreach ($extraColumns as $k => $v) { - $columns[$k] = $v; - } - if (is_array($relation->via)) { - /** @var $viaClass ActiveRecordInterface */ - /** @var $record ActiveRecordInterface */ - $record = new $viaClass(); - foreach ($columns as $column => $value) { - $record->$column = $value; - } - $record->insert(false); - } else { - /** @var $viaTable string */ - static::getDb()->createCommand() - ->insert($viaTable, $columns)->execute(); - } - } else { - $p1 = $model->isPrimaryKey(array_keys($relation->link)); - $p2 = $this->isPrimaryKey(array_values($relation->link)); - if ($p1 && $p2) { - if ($this->getIsNewRecord() && $model->getIsNewRecord()) { - throw new InvalidCallException('Unable to link models: both models are newly created.'); - } elseif ($this->getIsNewRecord()) { - $this->bindModels(array_flip($relation->link), $this, $model); - } else { - $this->bindModels($relation->link, $model, $this); - } - } elseif ($p1) { - $this->bindModels(array_flip($relation->link), $this, $model); - } elseif ($p2) { - $this->bindModels($relation->link, $model, $this); - } else { - throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); - } - } - - // update lazily loaded related objects - if (!$relation->multiple) { - $this->_related[$name] = $model; - } elseif (isset($this->_related[$name])) { - if ($relation->indexBy !== null) { - $indexBy = $relation->indexBy; - $this->_related[$name][$model->$indexBy] = $model; - } else { - $this->_related[$name][] = $model; - } - } - } - - /** - * Destroys the relationship between two models. - * - * The model with the foreign key of the relationship will be deleted if `$delete` is true. - * Otherwise, the foreign key will be set null and the model will be saved without validation. - * - * @param string $name the case sensitive name of the relationship. - * @param ActiveRecordInterface $model the model to be unlinked from the current one. - * @param boolean $delete whether to delete the model that contains the foreign key. - * If false, the model's foreign key will be set null and saved. - * If true, the model containing the foreign key will be deleted. - * @throws InvalidCallException if the models cannot be unlinked - */ - public function unlink($name, $model, $delete = false) - { - $relation = $this->getRelation($name); - - if ($relation->via !== null) { - if (is_array($relation->via)) { - /** @var ActiveQuery $viaRelation */ - list($viaName, $viaRelation) = $relation->via; - $viaClass = $viaRelation->modelClass; - unset($this->_related[$viaName]); - } else { - $viaRelation = $relation->via; - $viaTable = reset($relation->via->from); - } - $columns = []; - foreach ($viaRelation->link as $a => $b) { - $columns[$a] = $this->$b; - } - foreach ($relation->link as $a => $b) { - $columns[$b] = $model->$a; - } - if (is_array($relation->via)) { - /** @var $viaClass ActiveRecordInterface */ - if ($delete) { - $viaClass::deleteAll($columns); - } else { - $nulls = []; - foreach (array_keys($columns) as $a) { - $nulls[$a] = null; - } - $viaClass::updateAll($nulls, $columns); - } - } else { - /** @var $viaTable string */ - /** @var Command $command */ - $command = static::getDb()->createCommand(); - if ($delete) { - $command->delete($viaTable, $columns)->execute(); - } else { - $nulls = []; - foreach (array_keys($columns) as $a) { - $nulls[$a] = null; - } - $command->update($viaTable, $nulls, $columns)->execute(); - } - } - } else { - $p1 = $model->isPrimaryKey(array_keys($relation->link)); - $p2 = $this->isPrimaryKey(array_values($relation->link)); - if ($p1 && $p2 || $p2) { - foreach ($relation->link as $a => $b) { - $model->$a = null; - } - $delete ? $model->delete() : $model->save(false); - } elseif ($p1) { - foreach ($relation->link as $b) { - $this->$b = null; - } - $delete ? $this->delete() : $this->save(false); - } else { - throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); - } - } - - if (!$relation->multiple) { - unset($this->_related[$name]); - } elseif (isset($this->_related[$name])) { - /** @var ActiveRecordInterface $b */ - foreach ($this->_related[$name] as $a => $b) { - if ($model->getPrimaryKey() == $b->getPrimaryKey()) { - unset($this->_related[$name][$a]); - } - } - } - } - - /** - * @param array $link - * @param BaseActiveRecord $foreignModel - * @param BaseActiveRecord $primaryModel - * @throws InvalidCallException - */ - private function bindModels($link, $foreignModel, $primaryModel) - { - foreach ($link as $fk => $pk) { - $value = $primaryModel->$pk; - if ($value === null) { - throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); - } - $foreignModel->$fk = $value; - } - $foreignModel->save(false); - } - - /** - * Returns a value indicating whether the given set of attributes represents the primary key for this model - * @param array $keys the set of attributes to check - * @return boolean whether the given set of attributes represents the primary key for this model - */ - public static function isPrimaryKey($keys) - { - $pks = static::primaryKey(); - if (count($keys) === count($pks)) { - return count(array_intersect($keys, $pks)) === count($pks); - } else { - return false; - } - } - - /** - * Returns the text label for the specified attribute. - * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model. - * @param string $attribute the attribute name - * @return string the attribute label - * @see generateAttributeLabel() - * @see attributeLabels() - */ - public function getAttributeLabel($attribute) - { - $labels = $this->attributeLabels(); - if (isset($labels[$attribute])) { - return ($labels[$attribute]); - } elseif (strpos($attribute, '.')) { - $attributeParts = explode('.', $attribute); - $neededAttribute = array_pop($attributeParts); - - $relatedModel = $this; - foreach ($attributeParts as $relationName) { - if (isset($this->_related[$relationName]) && $this->_related[$relationName] instanceof self) { - $relatedModel = $this->_related[$relationName]; - } else { - try { - $relation = $relatedModel->getRelation($relationName); - } catch (InvalidParamException $e) { - return $this->generateAttributeLabel($attribute); - } - $relatedModel = new $relation->modelClass; - } - } - - $labels = $relatedModel->attributeLabels(); - if (isset($labels[$neededAttribute])) { - return $labels[$neededAttribute]; - } - } - - return $this->generateAttributeLabel($attribute); - } - - /** - * @inheritdoc - * - * The default implementation returns the names of the columns whose values have been populated into this record. - */ - public function fields() - { - $fields = array_keys($this->_attributes); - return array_combine($fields, $fields); - } - - /** - * @inheritdoc - * - * The default implementation returns the names of the relations that have been populated into this record. - */ - public function extraFields() - { - $fields = array_keys($this->getRelatedRecords()); - return array_combine($fields, $fields); - } - - /** - * Sets the element value at the specified offset to null. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($model[$offset])`. - * @param mixed $offset the offset to unset element - */ - public function offsetUnset($offset) - { - if (property_exists($this, $offset)) { - $this->$offset = null; - } else { - unset($this->$offset); - } - } + /** + * @event Event an event that is triggered when the record is initialized via [[init()]]. + */ + const EVENT_INIT = 'init'; + /** + * @event Event an event that is triggered after the record is created and populated with query result. + */ + const EVENT_AFTER_FIND = 'afterFind'; + /** + * @event ModelEvent an event that is triggered before inserting a record. + * You may set [[ModelEvent::isValid]] to be false to stop the insertion. + */ + const EVENT_BEFORE_INSERT = 'beforeInsert'; + /** + * @event Event an event that is triggered after a record is inserted. + */ + const EVENT_AFTER_INSERT = 'afterInsert'; + /** + * @event ModelEvent an event that is triggered before updating a record. + * You may set [[ModelEvent::isValid]] to be false to stop the update. + */ + const EVENT_BEFORE_UPDATE = 'beforeUpdate'; + /** + * @event Event an event that is triggered after a record is updated. + */ + const EVENT_AFTER_UPDATE = 'afterUpdate'; + /** + * @event ModelEvent an event that is triggered before deleting a record. + * You may set [[ModelEvent::isValid]] to be false to stop the deletion. + */ + const EVENT_BEFORE_DELETE = 'beforeDelete'; + /** + * @event Event an event that is triggered after a record is deleted. + */ + const EVENT_AFTER_DELETE = 'afterDelete'; + + /** + * @var array attribute values indexed by attribute names + */ + private $_attributes = []; + /** + * @var array old attribute values indexed by attribute names. + */ + private $_oldAttributes; + /** + * @var array related models indexed by the relation names + */ + private $_related = []; + + /** + * Creates an [[ActiveQuery]] instance for query purpose. + * + * The returned [[ActiveQuery]] instance can be further customized by calling + * methods defined in [[ActiveQuery]] before `one()`, `all()` or `value()` is + * called to return the populated active records: + * + * ~~~ + * // find all customers + * $customers = Customer::find()->all(); + * + * // find all active customers and order them by their age: + * $customers = Customer::find() + * ->where(['status' => 1]) + * ->orderBy('age') + * ->all(); + * + * // find a single customer whose primary key value is 10 + * $customer = Customer::find(10); + * + * // the above is equivalent to: + * $customer = Customer::find()->where(['id' => 10])->one(); + * + * // find a single customer whose age is 30 and whose status is 1 + * $customer = Customer::find(['age' => 30, 'status' => 1]); + * + * // the above is equivalent to: + * $customer = Customer::find()->where(['age' => 30, 'status' => 1])->one(); + * ~~~ + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value and return the + * corresponding record. + * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. + * - null: return a new [[ActiveQuery]] object for further query purpose. + * + * @return ActiveQuery|static|null When `$q` is null, a new [[ActiveQuery]] instance + * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be + * returned (null will be returned if there is no matching). + * @throws InvalidConfigException if the AR class does not have a primary key + * @see createQuery() + */ + public static function find($q = null) + { + $query = static::createQuery(); + if (is_array($q)) { + return $query->andWhere($q)->one(); + } elseif ($q !== null) { + // query by primary key + $primaryKey = static::primaryKey(); + if (isset($primaryKey[0])) { + return $query->andWhere([$primaryKey[0] => $q])->one(); + } else { + throw new InvalidConfigException(get_called_class() . ' must have a primary key.'); + } + } + + return $query; + } + + /** + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], 'status = 2'); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = '') + { + throw new NotSupportedException(__METHOD__ . ' is not supported.'); + } + + /** + * Updates the whole table using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = '') + { + throw new NotSupportedException(__METHOD__ . ' is not supported.'); + } + + /** + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll('status = 3'); + * ~~~ + * + * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = '', $params = []) + { + throw new NotSupportedException(__METHOD__ . ' is not supported.'); + } + + /** + * Returns the name of the column that stores the lock version for implementing optimistic locking. + * + * Optimistic locking allows multiple users to access the same record for edits and avoids + * potential conflicts. In case when a user attempts to save the record upon some staled data + * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, + * and the update or deletion is skipped. + * + * Optimistic locking is only supported by [[update()]] and [[delete()]]. + * + * To use Optimistic locking: + * + * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. + * Override this method to return the name of this column. + * 2. In the Web form that collects the user input, add a hidden field that stores + * the lock version of the recording being updated. + * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] + * and implement necessary business logic (e.g. merging the changes, prompting stated data) + * to resolve the conflict. + * + * @return string the column name that stores the lock version of a table row. + * If null is returned (default implemented), optimistic locking will not be supported. + */ + public function optimisticLock() + { + return null; + } + + /** + * PHP getter magic method. + * This method is overridden so that attributes and related objects can be accessed like properties. + * + * @param string $name property name + * @throws \yii\base\InvalidParamException if relation name is wrong + * @return mixed property value + * @see getAttribute() + */ + public function __get($name) + { + if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { + return $this->_attributes[$name]; + } elseif ($this->hasAttribute($name)) { + return null; + } else { + if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) { + return $this->_related[$name]; + } + $value = parent::__get($name); + if ($value instanceof ActiveQueryInterface) { + return $this->_related[$name] = $value->findFor($name, $this); + } else { + return $value; + } + } + } + + /** + * PHP setter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string $name property name + * @param mixed $value property value + */ + public function __set($name, $value) + { + if ($this->hasAttribute($name)) { + $this->_attributes[$name] = $value; + } else { + parent::__set($name, $value); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking if the named attribute is null or not. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + try { + return $this->__get($name) !== null; + } catch (\Exception $e) { + return false; + } + } + + /** + * Sets a component property to be null. + * This method overrides the parent implementation by clearing + * the specified attribute value. + * @param string $name the property name or the event name + */ + public function __unset($name) + { + if ($this->hasAttribute($name)) { + unset($this->_attributes[$name]); + } elseif (array_key_exists($name, $this->_related)) { + unset($this->_related[$name]); + } elseif ($this->getRelation($name, false) === null) { + parent::__unset($name); + } + } + + /** + * Declares a `has-one` relation. + * The declaration is returned in terms of a relational [[ActiveQuery]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-one` relation means that there is at most one related record matching + * the criteria set by this relation, e.g., a customer has one country. + * + * For example, to declare the `country` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getCountry() + * { + * return $this->hasOne(Country::className(), ['id' => 'country_id']); + * } + * ~~~ + * + * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name + * in the related class `Country`, while the 'country_id' value refers to an attribute name + * in the current AR class. + * + * Call methods declared in [[ActiveQuery]] to further customize the relation. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the attributes of the record associated with the `$class` model, while the values of the + * array refer to the corresponding attributes in **this** AR class. + * @return ActiveQueryInterface the relational query object. + */ + public function hasOne($class, $link) + { + /** @var ActiveRecordInterface $class */ + + return $class::createQuery([ + 'modelClass' => $class, + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => false, + ]); + } + + /** + * Declares a `has-many` relation. + * The declaration is returned in terms of a relational [[ActiveQuery]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-many` relation means that there are multiple related records matching + * the criteria set by this relation, e.g., a customer has many orders. + * + * For example, to declare the `orders` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getOrders() + * { + * return $this->hasMany(Order::className(), ['customer_id' => 'id']); + * } + * ~~~ + * + * Note that in the above, the 'customer_id' key in the `$link` parameter refers to + * an attribute name in the related class `Order`, while the 'id' value refers to + * an attribute name in the current AR class. + * + * Call methods declared in [[ActiveQuery]] to further customize the relation. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the attributes of the record associated with the `$class` model, while the values of the + * array refer to the corresponding attributes in **this** AR class. + * @return ActiveQueryInterface the relational query object. + */ + public function hasMany($class, $link) + { + /** @var ActiveRecordInterface $class */ + + return $class::createQuery([ + 'modelClass' => $class, + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => true, + ]); + } + + /** + * Populates the named relation with the related records. + * Note that this method does not check if the relation exists or not. + * @param string $name the relation name (case-sensitive) + * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation. + */ + public function populateRelation($name, $records) + { + $this->_related[$name] = $records; + } + + /** + * Check whether the named relation has been populated with records. + * @param string $name the relation name (case-sensitive) + * @return boolean whether relation has been populated with records. + */ + public function isRelationPopulated($name) + { + return array_key_exists($name, $this->_related); + } + + /** + * Returns all populated related records. + * @return array an array of related records indexed by relation names. + */ + public function getRelatedRecords() + { + return $this->_related; + } + + /** + * Returns a value indicating whether the model has an attribute with the specified name. + * @param string $name the name of the attribute + * @return boolean whether the model has an attribute with the specified name. + */ + public function hasAttribute($name) + { + return isset($this->_attributes[$name]) || in_array($name, $this->attributes()); + } + + /** + * Returns the named attribute value. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * @param string $name the attribute name + * @return mixed the attribute value. Null if the attribute is not set or does not exist. + * @see hasAttribute() + */ + public function getAttribute($name) + { + return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; + } + + /** + * Sets the named attribute value. + * @param string $name the attribute name + * @param mixed $value the attribute value. + * @throws InvalidParamException if the named attribute does not exist. + * @see hasAttribute() + */ + public function setAttribute($name, $value) + { + if ($this->hasAttribute($name)) { + $this->_attributes[$name] = $value; + } else { + throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); + } + } + + /** + * Returns the old attribute values. + * @return array the old attribute values (name-value pairs) + */ + public function getOldAttributes() + { + return $this->_oldAttributes === null ? [] : $this->_oldAttributes; + } + + /** + * Sets the old attribute values. + * All existing old attribute values will be discarded. + * @param array $values old attribute values to be set. + */ + public function setOldAttributes($values) + { + $this->_oldAttributes = $values; + } + + /** + * Returns the old value of the named attribute. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * @param string $name the attribute name + * @return mixed the old attribute value. Null if the attribute is not loaded before + * or does not exist. + * @see hasAttribute() + */ + public function getOldAttribute($name) + { + return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; + } + + /** + * Sets the old value of the named attribute. + * @param string $name the attribute name + * @param mixed $value the old attribute value. + * @throws InvalidParamException if the named attribute does not exist. + * @see hasAttribute() + */ + public function setOldAttribute($name, $value) + { + if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) { + $this->_oldAttributes[$name] = $value; + } else { + throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); + } + } + + /** + * Marks an attribute dirty. + * This method may be called to force updating a record when calling [[update()]], + * even if there is no change being made to the record. + * @param string $name the attribute name + */ + public function markAttributeDirty($name) + { + unset($this->_oldAttributes[$name]); + } + + /** + * Returns a value indicating whether the named attribute has been changed. + * @param string $name the name of the attribute + * @return boolean whether the attribute has been changed + */ + public function isAttributeChanged($name) + { + if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { + return $this->_attributes[$name] !== $this->_oldAttributes[$name]; + } else { + return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]); + } + } + + /** + * Returns the attribute values that have been modified since they are loaded or saved most recently. + * @param string[]|null $names the names of the attributes whose values may be returned if they are + * changed recently. If null, [[attributes()]] will be used. + * @return array the changed attribute values (name-value pairs) + */ + public function getDirtyAttributes($names = null) + { + if ($names === null) { + $names = $this->attributes(); + } + $names = array_flip($names); + $attributes = []; + if ($this->_oldAttributes === null) { + foreach ($this->_attributes as $name => $value) { + if (isset($names[$name])) { + $attributes[$name] = $value; + } + } + } else { + foreach ($this->_attributes as $name => $value) { + if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { + $attributes[$name] = $value; + } + } + } + + return $attributes; + } + + /** + * Saves the current record. + * + * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] + * when [[isNewRecord]] is false. + * + * For example, to save a customer record: + * + * ~~~ + * $customer = new Customer; // or $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->save(); + * ~~~ + * + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation = true, $attributes = null) + { + if ($this->getIsNewRecord()) { + return $this->insert($runValidation, $attributes); + } else { + return $this->update($runValidation, $attributes) !== false; + } + } + + /** + * Saves the changes to this active record into the associated database table. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. save the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be saved into database. + * + * For example, to update a customer record: + * + * ~~~ + * $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->update(); + * ~~~ + * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being updated is outdated. + * @throws \Exception in case update failed. + */ + public function update($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + + return $this->updateInternal($attributes); + } + + /** + * Updates the specified attributes. + * + * This method is a shortcut to [[update()]] when data validation is not needed + * and only a list of attributes need to be updated. + * + * You may specify the attributes to be updated as name list or name-value pairs. + * If the latter, the corresponding attribute values will be modified accordingly. + * The method will then save the specified attributes into database. + * + * Note that this method will NOT perform data validation. + * + * @param array $attributes the attributes (names or name-value pairs) to be updated + * @return integer|boolean the number of rows affected, or false if [[beforeSave()]] stops the updating process. + */ + public function updateAttributes($attributes) + { + $attrs = []; + foreach ($attributes as $name => $value) { + if (is_integer($name)) { + $attrs[] = $value; + } else { + $this->$name = $value; + $attrs[] = $name; + } + } + + return $this->update(false, $attrs); + } + + /** + * @see update() + * @throws StaleObjectException + */ + protected function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false); + + return 0; + } + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } + // We do not check the return value of updateAll() because it's possible + // that the UPDATE statement doesn't change anything and thus returns 0. + $rows = $this->updateAll($values, $condition); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + $this->afterSave(false); + + return $rows; + } + + /** + * Updates one or several counter columns for the current AR object. + * Note that this method differs from [[updateAllCounters()]] in that it only + * saves counters for the current AR object. + * + * An example usage is as follows: + * + * ~~~ + * $post = Post::find($id); + * $post->updateCounters(['view_count' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value) + * Use negative values if you want to decrement the counters. + * @return boolean whether the saving is successful + * @see updateAllCounters() + */ + public function updateCounters($counters) + { + if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) { + foreach ($counters as $name => $value) { + $this->_attributes[$name] += $value; + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + + return true; + } else { + return false; + } + } + + /** + * Deletes the table row corresponding to this active record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeDelete()]]. If the method returns false, it will skip the + * rest of the steps; + * 2. delete the record from the database; + * 3. call [[afterDelete()]]. + * + * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] + * will be raised by the corresponding methods. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. + * @throws \Exception in case delete failed. + */ + public function delete() + { + $result = false; + if ($this->beforeDelete()) { + // we do not check the return value of deleteAll() because it's possible + // the record is already deleted in the database and thus the method will return 0 + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = $this->deleteAll($condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->_oldAttributes = null; + $this->afterDelete(); + } + + return $result; + } + + /** + * Returns a value indicating whether the current record is new. + * @return boolean whether the record is new and should be inserted when calling [[save()]]. + */ + public function getIsNewRecord() + { + return $this->_oldAttributes === null; + } + + /** + * Sets the value indicating whether the record is new. + * @param boolean $value whether the record is new and should be inserted when calling [[save()]]. + * @see getIsNewRecord() + */ + public function setIsNewRecord($value) + { + $this->_oldAttributes = $value ? null : $this->_attributes; + } + + /** + * Initializes the object. + * This method is called at the end of the constructor. + * The default implementation will trigger an [[EVENT_INIT]] event. + * If you override this method, make sure you call the parent implementation at the end + * to ensure triggering of the event. + */ + public function init() + { + parent::init(); + $this->trigger(self::EVENT_INIT); + } + + /** + * This method is called when the AR object is created and populated with the query result. + * The default implementation will trigger an [[EVENT_AFTER_FIND]] event. + * When overriding this method, make sure you call the parent implementation to ensure the + * event is triggered. + */ + public function afterFind() + { + $this->trigger(self::EVENT_AFTER_FIND); + } + + /** + * This method is called at the beginning of inserting or updating a record. + * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true, + * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false. + * When overriding this method, make sure you call the parent implementation like the following: + * + * ~~~ + * public function beforeSave($insert) + * { + * if (parent::beforeSave($insert)) { + * // ...custom code here... + * return true; + * } else { + * return false; + * } + * } + * ~~~ + * + * @param boolean $insert whether this method called while inserting a record. + * If false, it means the method is called while updating a record. + * @return boolean whether the insertion or updating should continue. + * If false, the insertion or updating will be cancelled. + */ + public function beforeSave($insert) + { + $event = new ModelEvent; + $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); + + return $event->isValid; + } + + /** + * This method is called at the end of inserting or updating a record. + * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true, + * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false. + * When overriding this method, make sure you call the parent implementation so that + * the event is triggered. + * @param boolean $insert whether this method called while inserting a record. + * If false, it means the method is called while updating a record. + */ + public function afterSave($insert) + { + $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE); + } + + /** + * This method is invoked before deleting a record. + * The default implementation raises the [[EVENT_BEFORE_DELETE]] event. + * When overriding this method, make sure you call the parent implementation like the following: + * + * ~~~ + * public function beforeDelete() + * { + * if (parent::beforeDelete()) { + * // ...custom code here... + * return true; + * } else { + * return false; + * } + * } + * ~~~ + * + * @return boolean whether the record should be deleted. Defaults to true. + */ + public function beforeDelete() + { + $event = new ModelEvent; + $this->trigger(self::EVENT_BEFORE_DELETE, $event); + + return $event->isValid; + } + + /** + * This method is invoked after deleting a record. + * The default implementation raises the [[EVENT_AFTER_DELETE]] event. + * You may override this method to do postprocessing after the record is deleted. + * Make sure you call the parent implementation so that the event is raised properly. + */ + public function afterDelete() + { + $this->trigger(self::EVENT_AFTER_DELETE); + } + + /** + * Repopulates this active record with the latest data. + * @return boolean whether the row still exists in the database. If true, the latest data + * will be populated to this active record. Otherwise, this record will remain unchanged. + */ + public function refresh() + { + $record = $this->find($this->getPrimaryKey(true)); + if ($record === null) { + return false; + } + foreach ($this->attributes() as $name) { + $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null; + } + $this->_oldAttributes = $this->_attributes; + $this->_related = []; + + return true; + } + + /** + * Returns a value indicating whether the given active record is the same as the current one. + * The comparison is made by comparing the table names and the primary key values of the two active records. + * If one of the records [[isNewRecord|is new]] they are also considered not equal. + * @param ActiveRecordInterface $record record to compare to + * @return boolean whether the two active records refer to the same row in the same database table. + */ + public function equals($record) + { + if ($this->getIsNewRecord() || $record->getIsNewRecord()) { + return false; + } + + return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey(); + } + + /** + * Returns the primary key value(s). + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column names as keys and column values as values. + * Note that for composite primary keys, an array will always be returned regardless of this parameter value. + * @property mixed The primary key value. An array (column name => column value) is returned if + * the primary key is composite. A string is returned otherwise (null will be returned if + * the key value is null). + * @return mixed the primary key value. An array (column name => column value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getPrimaryKey($asArray = false) + { + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; + } else { + $values = []; + foreach ($keys as $name) { + $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; + } + + return $values; + } + } + + /** + * Returns the old primary key value(s). + * This refers to the primary key value that is populated into the record + * after executing a find method (e.g. find(), findAll()). + * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column name as key and column value as value. + * If this is false (default), a scalar value will be returned for non-composite primary key. + * @property mixed The old primary key value. An array (column name => column value) is + * returned if the primary key is composite. A string is returned otherwise (null will be + * returned if the key value is null). + * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getOldPrimaryKey($asArray = false) + { + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; + } else { + $values = []; + foreach ($keys as $name) { + $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; + } + + return $values; + } + } + + /** + * Populates an active record object using a row of data from the database/storage. + * + * This is an internal method meant to be called to create active record objects after + * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate + * the query results into active records. + * + * When calling this method manually you should call [[afterFind()]] on the created + * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]]. + * + * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance + * created by [[instantiate()]] beforehand. + * @param array $row attribute values (name => value) + */ + public static function populateRecord($record, $row) + { + $columns = array_flip($record->attributes()); + foreach ($row as $name => $value) { + if (isset($columns[$name])) { + $record->_attributes[$name] = $value; + } else { + $record->$name = $value; + } + } + $record->_oldAttributes = $record->_attributes; + } + + /** + * Creates an active record instance. + * + * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. + * It is not meant to be used for creating new records directly. + * + * You may override this method if the instance being created + * depends on the row data to be populated into the record. + * For example, by creating a record based on the value of a column, + * you may implement the so-called single-table inheritance mapping. + * @param array $row row data to be populated into the record. + * @return static the newly created active record + */ + public static function instantiate($row) + { + return new static; + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean whether there is an element at the specified offset. + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } + + /** + * Returns the relation object with the specified name. + * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object. + * It can be declared in either the Active Record class itself or one of its behaviors. + * @param string $name the relation name + * @param boolean $throwException whether to throw exception if the relation does not exist. + * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist + * and `$throwException` is false, null will be returned. + * @throws InvalidParamException if the named relation does not exist. + */ + public function getRelation($name, $throwException = true) + { + $getter = 'get' . $name; + try { + // the relation could be defined in a behavior + $relation = $this->$getter(); + } catch (UnknownMethodException $e) { + if ($throwException) { + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); + } else { + return null; + } + } + if (!$relation instanceof ActiveQueryInterface) { + if ($throwException) { + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); + } else { + return null; + } + } + + if (method_exists($this, $getter)) { + // relation name is case sensitive, trying to validate it when the relation is defined within this class + $method = new \ReflectionMethod($this, $getter); + $realName = lcfirst(substr($method->getName(), 3)); + if ($realName !== $name) { + if ($throwException) { + throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\"."); + } else { + return null; + } + } + } + + return $relation; + } + + /** + * Establishes the relationship between two models. + * + * The relationship is established by setting the foreign key value(s) in one model + * to be the corresponding primary key value(s) in the other model. + * The model with the foreign key will be saved into database without performing validation. + * + * If the relationship involves a pivot table, a new row will be inserted into the + * pivot table which contains the primary key values from both models. + * + * Note that this method requires that the primary key value is not null. + * + * @param string $name the case sensitive name of the relationship + * @param ActiveRecordInterface $model the model to be linked with the current one. + * @param array $extraColumns additional column values to be saved into the pivot table. + * This parameter is only meaningful for a relationship involving a pivot table + * (i.e., a relation set with [[ActiveRelationTrait::via()]] or `[[ActiveQuery::viaTable()]]`.) + * @throws InvalidCallException if the method is unable to link two models. + */ + public function link($name, $model, $extraColumns = []) + { + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if ($this->getIsNewRecord() || $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models must NOT be newly created.'); + } + if (is_array($relation->via)) { + /** @var ActiveQuery $viaRelation */ + list($viaName, $viaRelation) = $relation->via; + $viaClass = $viaRelation->modelClass; + // unset $viaName so that it can be reloaded to reflect the change + unset($this->_related[$viaName]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = []; + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + foreach ($extraColumns as $k => $v) { + $columns[$k] = $v; + } + if (is_array($relation->via)) { + /** @var $viaClass ActiveRecordInterface */ + /** @var $record ActiveRecordInterface */ + $record = new $viaClass(); + foreach ($columns as $column => $value) { + $record->$column = $value; + } + $record->insert(false); + } else { + /** @var $viaTable string */ + static::getDb()->createCommand() + ->insert($viaTable, $columns)->execute(); + } + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2) { + if ($this->getIsNewRecord() && $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models are newly created.'); + } elseif ($this->getIsNewRecord()) { + $this->bindModels(array_flip($relation->link), $this, $model); + } else { + $this->bindModels($relation->link, $model, $this); + } + } elseif ($p1) { + $this->bindModels(array_flip($relation->link), $this, $model); + } elseif ($p2) { + $this->bindModels($relation->link, $model, $this); + } else { + throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); + } + } + + // update lazily loaded related objects + if (!$relation->multiple) { + $this->_related[$name] = $model; + } elseif (isset($this->_related[$name])) { + if ($relation->indexBy !== null) { + $indexBy = $relation->indexBy; + $this->_related[$name][$model->$indexBy] = $model; + } else { + $this->_related[$name][] = $model; + } + } + } + + /** + * Destroys the relationship between two models. + * + * The model with the foreign key of the relationship will be deleted if `$delete` is true. + * Otherwise, the foreign key will be set null and the model will be saved without validation. + * + * @param string $name the case sensitive name of the relationship. + * @param ActiveRecordInterface $model the model to be unlinked from the current one. + * @param boolean $delete whether to delete the model that contains the foreign key. + * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. + * @throws InvalidCallException if the models cannot be unlinked + */ + public function unlink($name, $model, $delete = false) + { + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if (is_array($relation->via)) { + /** @var ActiveQuery $viaRelation */ + list($viaName, $viaRelation) = $relation->via; + $viaClass = $viaRelation->modelClass; + unset($this->_related[$viaName]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = []; + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + if (is_array($relation->via)) { + /** @var $viaClass ActiveRecordInterface */ + if ($delete) { + $viaClass::deleteAll($columns); + } else { + $nulls = []; + foreach (array_keys($columns) as $a) { + $nulls[$a] = null; + } + $viaClass::updateAll($nulls, $columns); + } + } else { + /** @var $viaTable string */ + /** @var Command $command */ + $command = static::getDb()->createCommand(); + if ($delete) { + $command->delete($viaTable, $columns)->execute(); + } else { + $nulls = []; + foreach (array_keys($columns) as $a) { + $nulls[$a] = null; + } + $command->update($viaTable, $nulls, $columns)->execute(); + } + } + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2 || $p2) { + foreach ($relation->link as $a => $b) { + $model->$a = null; + } + $delete ? $model->delete() : $model->save(false); + } elseif ($p1) { + foreach ($relation->link as $b) { + $this->$b = null; + } + $delete ? $this->delete() : $this->save(false); + } else { + throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); + } + } + + if (!$relation->multiple) { + unset($this->_related[$name]); + } elseif (isset($this->_related[$name])) { + /** @var ActiveRecordInterface $b */ + foreach ($this->_related[$name] as $a => $b) { + if ($model->getPrimaryKey() == $b->getPrimaryKey()) { + unset($this->_related[$name][$a]); + } + } + } + } + + /** + * @param array $link + * @param BaseActiveRecord $foreignModel + * @param BaseActiveRecord $primaryModel + * @throws InvalidCallException + */ + private function bindModels($link, $foreignModel, $primaryModel) + { + foreach ($link as $fk => $pk) { + $value = $primaryModel->$pk; + if ($value === null) { + throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); + } + $foreignModel->$fk = $value; + } + $foreignModel->save(false); + } + + /** + * Returns a value indicating whether the given set of attributes represents the primary key for this model + * @param array $keys the set of attributes to check + * @return boolean whether the given set of attributes represents the primary key for this model + */ + public static function isPrimaryKey($keys) + { + $pks = static::primaryKey(); + if (count($keys) === count($pks)) { + return count(array_intersect($keys, $pks)) === count($pks); + } else { + return false; + } + } + + /** + * Returns the text label for the specified attribute. + * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model. + * @param string $attribute the attribute name + * @return string the attribute label + * @see generateAttributeLabel() + * @see attributeLabels() + */ + public function getAttributeLabel($attribute) + { + $labels = $this->attributeLabels(); + if (isset($labels[$attribute])) { + return ($labels[$attribute]); + } elseif (strpos($attribute, '.')) { + $attributeParts = explode('.', $attribute); + $neededAttribute = array_pop($attributeParts); + + $relatedModel = $this; + foreach ($attributeParts as $relationName) { + if (isset($this->_related[$relationName]) && $this->_related[$relationName] instanceof self) { + $relatedModel = $this->_related[$relationName]; + } else { + try { + $relation = $relatedModel->getRelation($relationName); + } catch (InvalidParamException $e) { + return $this->generateAttributeLabel($attribute); + } + $relatedModel = new $relation->modelClass; + } + } + + $labels = $relatedModel->attributeLabels(); + if (isset($labels[$neededAttribute])) { + return $labels[$neededAttribute]; + } + } + + return $this->generateAttributeLabel($attribute); + } + + /** + * @inheritdoc + * + * The default implementation returns the names of the columns whose values have been populated into this record. + */ + public function fields() + { + $fields = array_keys($this->_attributes); + + return array_combine($fields, $fields); + } + + /** + * @inheritdoc + * + * The default implementation returns the names of the relations that have been populated into this record. + */ + public function extraFields() + { + $fields = array_keys($this->getRelatedRecords()); + + return array_combine($fields, $fields); + } + + /** + * Sets the element value at the specified offset to null. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($model[$offset])`. + * @param mixed $offset the offset to unset element + */ + public function offsetUnset($offset) + { + if (property_exists($this, $offset)) { + $this->$offset = null; + } else { + unset($this->$offset); + } + } } diff --git a/framework/db/BatchQueryResult.php b/framework/db/BatchQueryResult.php index 3a1ba117ef8..3c98d65eefc 100644 --- a/framework/db/BatchQueryResult.php +++ b/framework/db/BatchQueryResult.php @@ -30,148 +30,148 @@ */ class BatchQueryResult extends Object implements \Iterator { - /** - * @var Connection the DB connection to be used when performing batch query. - * If null, the "db" application component will be used. - */ - public $db; - /** - * @var Query the query object associated with this batch query. - * Do not modify this property directly unless after [[reset()]] is called explicitly. - */ - public $query; - /** - * @var integer the number of rows to be returned in each batch. - */ - public $batchSize = 100; - /** - * @var boolean whether to return a single row during each iteration. - * If false, a whole batch of rows will be returned in each iteration. - */ - public $each = false; - /** - * @var DataReader the data reader associated with this batch query. - */ - private $_dataReader; - /** - * @var array the data retrieved in the current batch - */ - private $_batch; - /** - * @var mixed the value for the current iteration - */ - private $_value; - /** - * @var string|integer the key for the current iteration - */ - private $_key; + /** + * @var Connection the DB connection to be used when performing batch query. + * If null, the "db" application component will be used. + */ + public $db; + /** + * @var Query the query object associated with this batch query. + * Do not modify this property directly unless after [[reset()]] is called explicitly. + */ + public $query; + /** + * @var integer the number of rows to be returned in each batch. + */ + public $batchSize = 100; + /** + * @var boolean whether to return a single row during each iteration. + * If false, a whole batch of rows will be returned in each iteration. + */ + public $each = false; + /** + * @var DataReader the data reader associated with this batch query. + */ + private $_dataReader; + /** + * @var array the data retrieved in the current batch + */ + private $_batch; + /** + * @var mixed the value for the current iteration + */ + private $_value; + /** + * @var string|integer the key for the current iteration + */ + private $_key; - /** - * Destructor. - */ - public function __destruct() - { - // make sure cursor is closed - $this->reset(); - } + /** + * Destructor. + */ + public function __destruct() + { + // make sure cursor is closed + $this->reset(); + } - /** - * Resets the batch query. - * This method will clean up the existing batch query so that a new batch query can be performed. - */ - public function reset() - { - if ($this->_dataReader !== null) { - $this->_dataReader->close(); - } - $this->_dataReader = null; - $this->_batch = null; - $this->_value = null; - $this->_key = null; - } + /** + * Resets the batch query. + * This method will clean up the existing batch query so that a new batch query can be performed. + */ + public function reset() + { + if ($this->_dataReader !== null) { + $this->_dataReader->close(); + } + $this->_dataReader = null; + $this->_batch = null; + $this->_value = null; + $this->_key = null; + } - /** - * Resets the iterator to the initial state. - * This method is required by the interface Iterator. - */ - public function rewind() - { - $this->reset(); - $this->next(); - } + /** + * Resets the iterator to the initial state. + * This method is required by the interface Iterator. + */ + public function rewind() + { + $this->reset(); + $this->next(); + } - /** - * Moves the internal pointer to the next dataset. - * This method is required by the interface Iterator. - */ - public function next() - { - if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) { - $this->_batch = $this->fetchData(); - reset($this->_batch); - } + /** + * Moves the internal pointer to the next dataset. + * This method is required by the interface Iterator. + */ + public function next() + { + if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) { + $this->_batch = $this->fetchData(); + reset($this->_batch); + } - if ($this->each) { - $this->_value = current($this->_batch); - if ($this->query->indexBy !== null) { - $this->_key = key($this->_batch); - } elseif (key($this->_batch) !== null) { - $this->_key++; - } else { - $this->_key = null; - } - } else { - $this->_value = $this->_batch; - $this->_key = $this->_key === null ? 0 : $this->_key + 1; - } - } + if ($this->each) { + $this->_value = current($this->_batch); + if ($this->query->indexBy !== null) { + $this->_key = key($this->_batch); + } elseif (key($this->_batch) !== null) { + $this->_key++; + } else { + $this->_key = null; + } + } else { + $this->_value = $this->_batch; + $this->_key = $this->_key === null ? 0 : $this->_key + 1; + } + } - /** - * Fetches the next batch of data. - * @return array the data fetched - */ - protected function fetchData() - { - if ($this->_dataReader === null) { - $this->_dataReader = $this->query->createCommand($this->db)->query(); - } + /** + * Fetches the next batch of data. + * @return array the data fetched + */ + protected function fetchData() + { + if ($this->_dataReader === null) { + $this->_dataReader = $this->query->createCommand($this->db)->query(); + } - $rows = []; - $count = 0; - while ($count++ < $this->batchSize && ($row = $this->_dataReader->read())) { - $rows[] = $row; - } + $rows = []; + $count = 0; + while ($count++ < $this->batchSize && ($row = $this->_dataReader->read())) { + $rows[] = $row; + } - return $this->query->prepareResult($rows); - } + return $this->query->prepareResult($rows); + } - /** - * Returns the index of the current dataset. - * This method is required by the interface Iterator. - * @return integer the index of the current row. - */ - public function key() - { - return $this->_key; - } + /** + * Returns the index of the current dataset. + * This method is required by the interface Iterator. + * @return integer the index of the current row. + */ + public function key() + { + return $this->_key; + } - /** - * Returns the current dataset. - * This method is required by the interface Iterator. - * @return mixed the current dataset. - */ - public function current() - { - return $this->_value; - } + /** + * Returns the current dataset. + * This method is required by the interface Iterator. + * @return mixed the current dataset. + */ + public function current() + { + return $this->_value; + } - /** - * Returns whether there is a valid dataset at the current position. - * This method is required by the interface Iterator. - * @return boolean whether there is a valid dataset at the current position. - */ - public function valid() - { - return !empty($this->_batch); - } + /** + * Returns whether there is a valid dataset at the current position. + * This method is required by the interface Iterator. + * @return boolean whether there is a valid dataset at the current position. + */ + public function valid() + { + return !empty($this->_batch); + } } diff --git a/framework/db/ColumnSchema.php b/framework/db/ColumnSchema.php index 3e7f6cf43f6..83dce13c109 100644 --- a/framework/db/ColumnSchema.php +++ b/framework/db/ColumnSchema.php @@ -17,90 +17,90 @@ */ class ColumnSchema extends Object { - /** - * @var string name of this column (without quotes). - */ - public $name; - /** - * @var boolean whether this column can be null. - */ - public $allowNull; - /** - * @var string abstract type of this column. Possible abstract types include: - * string, text, boolean, smallint, integer, bigint, float, decimal, datetime, - * timestamp, time, date, binary, and money. - */ - public $type; - /** - * @var string the PHP type of this column. Possible PHP types include: - * string, boolean, integer, double. - */ - public $phpType; - /** - * @var string the DB type of this column. Possible DB types vary according to the type of DBMS. - */ - public $dbType; - /** - * @var mixed default value of this column - */ - public $defaultValue; - /** - * @var array enumerable values. This is set only if the column is declared to be an enumerable type. - */ - public $enumValues; - /** - * @var integer display size of the column. - */ - public $size; - /** - * @var integer precision of the column data, if it is numeric. - */ - public $precision; - /** - * @var integer scale of the column data, if it is numeric. - */ - public $scale; - /** - * @var boolean whether this column is a primary key - */ - public $isPrimaryKey; - /** - * @var boolean whether this column is auto-incremental - */ - public $autoIncrement = false; - /** - * @var boolean whether this column is unsigned. This is only meaningful - * when [[type]] is `smallint`, `integer` or `bigint`. - */ - public $unsigned; - /** - * @var string comment of this column. Not all DBMS support this. - */ - public $comment; + /** + * @var string name of this column (without quotes). + */ + public $name; + /** + * @var boolean whether this column can be null. + */ + public $allowNull; + /** + * @var string abstract type of this column. Possible abstract types include: + * string, text, boolean, smallint, integer, bigint, float, decimal, datetime, + * timestamp, time, date, binary, and money. + */ + public $type; + /** + * @var string the PHP type of this column. Possible PHP types include: + * string, boolean, integer, double. + */ + public $phpType; + /** + * @var string the DB type of this column. Possible DB types vary according to the type of DBMS. + */ + public $dbType; + /** + * @var mixed default value of this column + */ + public $defaultValue; + /** + * @var array enumerable values. This is set only if the column is declared to be an enumerable type. + */ + public $enumValues; + /** + * @var integer display size of the column. + */ + public $size; + /** + * @var integer precision of the column data, if it is numeric. + */ + public $precision; + /** + * @var integer scale of the column data, if it is numeric. + */ + public $scale; + /** + * @var boolean whether this column is a primary key + */ + public $isPrimaryKey; + /** + * @var boolean whether this column is auto-incremental + */ + public $autoIncrement = false; + /** + * @var boolean whether this column is unsigned. This is only meaningful + * when [[type]] is `smallint`, `integer` or `bigint`. + */ + public $unsigned; + /** + * @var string comment of this column. Not all DBMS support this. + */ + public $comment; + /** + * Converts the input value according to [[phpType]]. + * If the value is null or an [[Expression]], it will not be converted. + * @param mixed $value input value + * @return mixed converted value + */ + public function typecast($value) + { + if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING && $this->type !== Schema::TYPE_BINARY) { + return null; + } + if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { + return $value; + } + switch ($this->phpType) { + case 'string': + return (string) $value; + case 'integer': + return (integer) $value; + case 'boolean': + return (boolean) $value; + } - /** - * Converts the input value according to [[phpType]]. - * If the value is null or an [[Expression]], it will not be converted. - * @param mixed $value input value - * @return mixed converted value - */ - public function typecast($value) - { - if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING && $this->type !== Schema::TYPE_BINARY) { - return null; - } - if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { - return $value; - } - switch ($this->phpType) { - case 'string': - return (string)$value; - case 'integer': - return (integer)$value; - case 'boolean': - return (boolean)$value; - } - return $value; - } + return $value; + } } diff --git a/framework/db/Command.php b/framework/db/Command.php index 6e3ddb7657b..bf29e72a909 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -53,704 +53,730 @@ */ class Command extends \yii\base\Component { - /** - * @var Connection the DB connection that this command is associated with - */ - public $db; - /** - * @var \PDOStatement the PDOStatement object that this command is associated with - */ - public $pdoStatement; - /** - * @var integer the default fetch mode for this command. - * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php - */ - public $fetchMode = \PDO::FETCH_ASSOC; - /** - * @var array the parameters (name => value) that are bound to the current PDO statement. - * This property is maintained by methods such as [[bindValue()]]. - * Do not modify it directly. - */ - public $params = []; - /** - * @var string the SQL statement that this command represents - */ - private $_sql; - - /** - * Returns the SQL statement for this command. - * @return string the SQL statement to be executed - */ - public function getSql() - { - return $this->_sql; - } - - /** - * Specifies the SQL statement to be executed. - * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well. - * @param string $sql the SQL statement to be set. - * @return static this command instance - */ - public function setSql($sql) - { - if ($sql !== $this->_sql) { - $this->cancel(); - $this->_sql = $this->db->quoteSql($sql); - $this->params = []; - } - return $this; - } - - /** - * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]]. - * Note that the return value of this method should mainly be used for logging purpose. - * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders. - * @return string the raw SQL with parameter values inserted into the corresponding placeholders in [[sql]]. - */ - public function getRawSql() - { - if (empty($this->params)) { - return $this->_sql; - } else { - $params = []; - foreach ($this->params as $name => $value) { - if (is_string($value)) { - $params[$name] = $this->db->quoteValue($value); - } elseif ($value === null) { - $params[$name] = 'NULL'; - } else { - $params[$name] = $value; - } - } - if (isset($params[1])) { - $sql = ''; - foreach (explode('?', $this->_sql) as $i => $part) { - $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; - } - return $sql; - } else { - return strtr($this->_sql, $params); - } - } - } - - /** - * Prepares the SQL statement to be executed. - * For complex SQL statement that is to be executed multiple times, - * this may improve performance. - * For SQL statement with binding parameters, this method is invoked - * automatically. - * @throws Exception if there is any DB error - */ - public function prepare() - { - if ($this->pdoStatement == null) { - $sql = $this->getSql(); - try { - $this->pdoStatement = $this->db->pdo->prepare($sql); - } catch (\Exception $e) { - $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; - $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, $errorInfo, (int)$e->getCode(), $e); - } - } - } - - /** - * Cancels the execution of the SQL statement. - * This method mainly sets [[pdoStatement]] to be null. - */ - public function cancel() - { - $this->pdoStatement = null; - } - - /** - * Binds a parameter to the SQL statement to be executed. - * @param string|integer $name parameter identifier. For a prepared statement - * using named placeholders, this will be a parameter name of - * the form `:name`. For a prepared statement using question mark - * placeholders, this will be the 1-indexed position of the parameter. - * @param mixed $value Name of the PHP variable to bind to the SQL statement parameter - * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. - * @param integer $length length of the data type - * @param mixed $driverOptions the driver-specific options - * @return static the current command being executed - * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php - */ - public function bindParam($name, &$value, $dataType = null, $length = null, $driverOptions = null) - { - $this->prepare(); - if ($dataType === null) { - $dataType = $this->db->getSchema()->getPdoType($value); - } - if ($length === null) { - $this->pdoStatement->bindParam($name, $value, $dataType); - } elseif ($driverOptions === null) { - $this->pdoStatement->bindParam($name, $value, $dataType, $length); - } else { - $this->pdoStatement->bindParam($name, $value, $dataType, $length, $driverOptions); - } - $this->params[$name] =& $value; - return $this; - } - - /** - * Binds a value to a parameter. - * @param string|integer $name Parameter identifier. For a prepared statement - * using named placeholders, this will be a parameter name of - * the form `:name`. For a prepared statement using question mark - * placeholders, this will be the 1-indexed position of the parameter. - * @param mixed $value The value to bind to the parameter - * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. - * @return static the current command being executed - * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php - */ - public function bindValue($name, $value, $dataType = null) - { - $this->prepare(); - if ($dataType === null) { - $dataType = $this->db->getSchema()->getPdoType($value); - } - $this->pdoStatement->bindValue($name, $value, $dataType); - $this->params[$name] = $value; - return $this; - } - - /** - * Binds a list of values to the corresponding parameters. - * This is similar to [[bindValue()]] except that it binds multiple values at a time. - * Note that the SQL data type of each value is determined by its PHP type. - * @param array $values the values to be bound. This must be given in terms of an associative - * array with array keys being the parameter names, and array values the corresponding parameter values, - * e.g. `[':name' => 'John', ':age' => 25]`. By default, the PDO type of each value is determined - * by its PHP type. You may explicitly specify the PDO type by using an array: `[value, type]`, - * e.g. `[':name' => 'John', ':profile' => [$profile, \PDO::PARAM_LOB]]`. - * @return static the current command being executed - */ - public function bindValues($values) - { - if (!empty($values)) { - $this->prepare(); - foreach ($values as $name => $value) { - if (is_array($value)) { - $type = $value[1]; - $value = $value[0]; - } else { - $type = $this->db->getSchema()->getPdoType($value); - } - $this->pdoStatement->bindValue($name, $value, $type); - $this->params[$name] = $value; - } - } - return $this; - } - - /** - * Executes the SQL statement. - * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. - * No result set will be returned. - * @return integer number of rows affected by the execution. - * @throws Exception execution failed - */ - public function execute() - { - $sql = $this->getSql(); - - $rawSql = $this->getRawSql(); - - Yii::info($rawSql, __METHOD__); - - if ($sql == '') { - return 0; - } - - $token = $rawSql; - try { - Yii::beginProfile($token, __METHOD__); - - $this->prepare(); - $this->pdoStatement->execute(); - $n = $this->pdoStatement->rowCount(); - - Yii::endProfile($token, __METHOD__); - return $n; - } catch (\Exception $e) { - Yii::endProfile($token, __METHOD__); - if ($e instanceof Exception) { - throw $e; - } else { - $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; - $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, $errorInfo, (int)$e->getCode(), $e); - } - } - } - - /** - * Executes the SQL statement and returns query result. - * This method is for executing a SQL query that returns result set, such as `SELECT`. - * @return DataReader the reader object for fetching the query result - * @throws Exception execution failed - */ - public function query() - { - return $this->queryInternal(''); - } - - /** - * Executes the SQL statement and returns ALL rows at once. - * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) - * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. - * @return array all rows of the query result. Each array element is an array representing a row of data. - * An empty array is returned if the query results in nothing. - * @throws Exception execution failed - */ - public function queryAll($fetchMode = null) - { - return $this->queryInternal('fetchAll', $fetchMode); - } - - /** - * Executes the SQL statement and returns the first row of the result. - * This method is best used when only the first row of result is needed for a query. - * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) - * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - * @throws Exception execution failed - */ - public function queryOne($fetchMode = null) - { - return $this->queryInternal('fetch', $fetchMode); - } - - /** - * Executes the SQL statement and returns the value of the first column in the first row of data. - * This method is best used when only a single value is needed for a query. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if there is no value. - * @throws Exception execution failed - */ - public function queryScalar() - { - $result = $this->queryInternal('fetchColumn', 0); - if (is_resource($result) && get_resource_type($result) === 'stream') { - return stream_get_contents($result); - } else { - return $result; - } - } - - /** - * Executes the SQL statement and returns the first column of the result. - * This method is best used when only the first column of result (i.e. the first element in each row) - * is needed for a query. - * @return array the first column of the query result. Empty array is returned if the query results in nothing. - * @throws Exception execution failed - */ - public function queryColumn() - { - return $this->queryInternal('fetchAll', \PDO::FETCH_COLUMN); - } - - /** - * Performs the actual DB query of a SQL statement. - * @param string $method method of PDOStatement to be called - * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) - * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. - * @return mixed the method execution result - * @throws Exception if the query causes any problem - */ - private function queryInternal($method, $fetchMode = null) - { - $db = $this->db; - $rawSql = $this->getRawSql(); - - Yii::info($rawSql, 'yii\db\Command::query'); - - /** @var \yii\caching\Cache $cache */ - if ($db->enableQueryCache && $method !== '') { - $cache = is_string($db->queryCache) ? Yii::$app->getComponent($db->queryCache) : $db->queryCache; - } - - if (isset($cache) && $cache instanceof Cache) { - $cacheKey = [ - __CLASS__, - $method, - $db->dsn, - $db->username, - $rawSql, - ]; - if (($result = $cache->get($cacheKey)) !== false) { - Yii::trace('Query result served from cache', 'yii\db\Command::query'); - return $result; - } - } - - $token = $rawSql; - try { - Yii::beginProfile($token, 'yii\db\Command::query'); - - $this->prepare(); - $this->pdoStatement->execute(); - - if ($method === '') { - $result = new DataReader($this); - } else { - if ($fetchMode === null) { - $fetchMode = $this->fetchMode; - } - $result = call_user_func_array([$this->pdoStatement, $method], (array)$fetchMode); - $this->pdoStatement->closeCursor(); - } - - Yii::endProfile($token, 'yii\db\Command::query'); - - if (isset($cache, $cacheKey) && $cache instanceof Cache) { - $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); - Yii::trace('Saved query result in cache', 'yii\db\Command::query'); - } - - return $result; - } catch (\Exception $e) { - Yii::endProfile($token, 'yii\db\Command::query'); - if ($e instanceof Exception) { - throw $e; - } else { - $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; - $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, $errorInfo, (int)$e->getCode(), $e); - } - } - } - - /** - * Creates an INSERT command. - * For example, - * - * ~~~ - * $connection->createCommand()->insert('tbl_user', [ - * 'name' => 'Sam', - * 'age' => 30, - * ])->execute(); - * ~~~ - * - * The method will properly escape the column names, and bind the values to be inserted. - * - * Note that the created command is not executed until [[execute()]] is called. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name => value) to be inserted into the table. - * @return Command the command object itself - */ - public function insert($table, $columns) - { - $params = []; - $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Creates a batch INSERT command. - * For example, - * - * ~~~ - * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ])->execute(); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the table - * @return Command the command object itself - */ - public function batchInsert($table, $columns, $rows) - { - $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows); - return $this->setSql($sql); - } - - /** - * Creates an UPDATE command. - * For example, - * - * ~~~ - * $connection->createCommand()->update('tbl_user', ['status' => 1], 'age > 30')->execute(); - * ~~~ - * - * The method will properly escape the column names and bind the values to be updated. - * - * Note that the created command is not executed until [[execute()]] is called. - * - * @param string $table the table to be updated. - * @param array $columns the column data (name => value) to be updated. - * @param string|array $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the parameters to be bound to the command - * @return Command the command object itself - */ - public function update($table, $columns, $condition = '', $params = []) - { - $sql = $this->db->getQueryBuilder()->update($table, $columns, $condition, $params); - return $this->setSql($sql)->bindValues($params); - } - - /** - * Creates a DELETE command. - * For example, - * - * ~~~ - * $connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); - * ~~~ - * - * The method will properly escape the table and column names. - * - * Note that the created command is not executed until [[execute()]] is called. - * - * @param string $table the table where the data will be deleted from. - * @param string|array $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the parameters to be bound to the command - * @return Command the command object itself - */ - public function delete($table, $condition = '', $params = []) - { - $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params); - return $this->setSql($sql)->bindValues($params); - } - - - /** - * Creates a SQL command for creating a new DB table. - * - * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), - * where name stands for a column name which will be properly quoted by the method, and definition - * stands for the column type which can contain an abstract DB type. - * The method [[QueryBuilder::getColumnType()]] will be called - * to convert the abstract column types to physical ones. For example, `string` will be converted - * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. - * - * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly - * inserted into the generated SQL. - * - * @param string $table the name of the table to be created. The name will be properly quoted by the method. - * @param array $columns the columns (name => definition) in the new table. - * @param string $options additional SQL fragment that will be appended to the generated SQL. - * @return Command the command object itself - */ - public function createTable($table, $columns, $options = null) - { - $sql = $this->db->getQueryBuilder()->createTable($table, $columns, $options); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for renaming a DB table. - * @param string $table the table to be renamed. The name will be properly quoted by the method. - * @param string $newName the new table name. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function renameTable($table, $newName) - { - $sql = $this->db->getQueryBuilder()->renameTable($table, $newName); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for dropping a DB table. - * @param string $table the table to be dropped. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function dropTable($table) - { - $sql = $this->db->getQueryBuilder()->dropTable($table); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for truncating a DB table. - * @param string $table the table to be truncated. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function truncateTable($table) - { - $sql = $this->db->getQueryBuilder()->truncateTable($table); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for adding a new DB column. - * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. - * @param string $column the name of the new column. The name will be properly quoted by the method. - * @param string $type the column type. [[\yii\db\QueryBuilder::getColumnType()]] will be called - * to convert the give column type to the physical one. For example, `string` will be converted - * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. - * @return Command the command object itself - */ - public function addColumn($table, $column, $type) - { - $sql = $this->db->getQueryBuilder()->addColumn($table, $column, $type); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for dropping a DB column. - * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. - * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function dropColumn($table, $column) - { - $sql = $this->db->getQueryBuilder()->dropColumn($table, $column); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for renaming a column. - * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. - * @param string $oldName the old name of the column. The name will be properly quoted by the method. - * @param string $newName the new name of the column. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function renameColumn($table, $oldName, $newName) - { - $sql = $this->db->getQueryBuilder()->renameColumn($table, $oldName, $newName); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for changing the definition of a column. - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the column type. [[\yii\db\QueryBuilder::getColumnType()]] will be called - * to convert the give column type to the physical one. For example, `string` will be converted - * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. - * @return Command the command object itself - */ - public function alterColumn($table, $column, $type) - { - $sql = $this->db->getQueryBuilder()->alterColumn($table, $column, $type); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for adding a primary key constraint to an existing table. - * The method will properly quote the table and column names. - * @param string $name the name of the primary key constraint. - * @param string $table the table that the primary key constraint will be added to. - * @param string|array $columns comma separated string or array of columns that the primary key will consist of. - * @return Command the command object itself. - */ - public function addPrimaryKey($name, $table, $columns) - { - $sql = $this->db->getQueryBuilder()->addPrimaryKey($name, $table, $columns); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for removing a primary key constraint to an existing table. - * @param string $name the name of the primary key constraint to be removed. - * @param string $table the table that the primary key constraint will be removed from. - * @return Command the command object itself - */ - public function dropPrimaryKey($name, $table) - { - $sql = $this->db->getQueryBuilder()->dropPrimaryKey($name, $table); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for adding a foreign key constraint to an existing table. - * The method will properly quote the table and column names. - * @param string $name the name of the foreign key constraint. - * @param string $table the table that the foreign key constraint will be added to. - * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. - * @param string $refTable the table that the foreign key references to. - * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. - * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @return Command the command object itself - */ - public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) - { - $sql = $this->db->getQueryBuilder()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for dropping a foreign key constraint. - * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function dropForeignKey($name, $table) - { - $sql = $this->db->getQueryBuilder()->dropForeignKey($name, $table); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for creating a new index. - * @param string $name the name of the index. The name will be properly quoted by the method. - * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. - * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns, please separate them - * by commas. The column names will be properly quoted by the method. - * @param boolean $unique whether to add UNIQUE constraint on the created index. - * @return Command the command object itself - */ - public function createIndex($name, $table, $columns, $unique = false) - { - $sql = $this->db->getQueryBuilder()->createIndex($name, $table, $columns, $unique); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for dropping an index. - * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. - * @return Command the command object itself - */ - public function dropIndex($name, $table) - { - $sql = $this->db->getQueryBuilder()->dropIndex($name, $table); - return $this->setSql($sql); - } - - /** - * Creates a SQL command for resetting the sequence value of a table's primary key. - * The sequence will be reset such that the primary key of the next new row inserted - * will have the specified value or 1. - * @param string $table the name of the table whose primary key sequence will be reset - * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, - * the next new row's primary key will have a value 1. - * @return Command the command object itself - * @throws NotSupportedException if this is not supported by the underlying DBMS - */ - public function resetSequence($table, $value = null) - { - $sql = $this->db->getQueryBuilder()->resetSequence($table, $value); - return $this->setSql($sql); - } - - /** - * Builds a SQL command for enabling or disabling integrity check. - * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema name of the tables. Defaults to empty string, meaning the current - * or default schema. - * @param string $table the table name. - * @return Command the command object itself - * @throws NotSupportedException if this is not supported by the underlying DBMS - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - $sql = $this->db->getQueryBuilder()->checkIntegrity($check, $schema, $table); - return $this->setSql($sql); - } + /** + * @var Connection the DB connection that this command is associated with + */ + public $db; + /** + * @var \PDOStatement the PDOStatement object that this command is associated with + */ + public $pdoStatement; + /** + * @var integer the default fetch mode for this command. + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + */ + public $fetchMode = \PDO::FETCH_ASSOC; + /** + * @var array the parameters (name => value) that are bound to the current PDO statement. + * This property is maintained by methods such as [[bindValue()]]. + * Do not modify it directly. + */ + public $params = []; + /** + * @var string the SQL statement that this command represents + */ + private $_sql; + + /** + * Returns the SQL statement for this command. + * @return string the SQL statement to be executed + */ + public function getSql() + { + return $this->_sql; + } + + /** + * Specifies the SQL statement to be executed. + * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well. + * @param string $sql the SQL statement to be set. + * @return static this command instance + */ + public function setSql($sql) + { + if ($sql !== $this->_sql) { + $this->cancel(); + $this->_sql = $this->db->quoteSql($sql); + $this->params = []; + } + + return $this; + } + + /** + * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]]. + * Note that the return value of this method should mainly be used for logging purpose. + * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders. + * @return string the raw SQL with parameter values inserted into the corresponding placeholders in [[sql]]. + */ + public function getRawSql() + { + if (empty($this->params)) { + return $this->_sql; + } else { + $params = []; + foreach ($this->params as $name => $value) { + if (is_string($value)) { + $params[$name] = $this->db->quoteValue($value); + } elseif ($value === null) { + $params[$name] = 'NULL'; + } else { + $params[$name] = $value; + } + } + if (isset($params[1])) { + $sql = ''; + foreach (explode('?', $this->_sql) as $i => $part) { + $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; + } + + return $sql; + } else { + return strtr($this->_sql, $params); + } + } + } + + /** + * Prepares the SQL statement to be executed. + * For complex SQL statement that is to be executed multiple times, + * this may improve performance. + * For SQL statement with binding parameters, this method is invoked + * automatically. + * @throws Exception if there is any DB error + */ + public function prepare() + { + if ($this->pdoStatement == null) { + $sql = $this->getSql(); + try { + $this->pdoStatement = $this->db->pdo->prepare($sql); + } catch (\Exception $e) { + $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; + $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; + throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); + } + } + } + + /** + * Cancels the execution of the SQL statement. + * This method mainly sets [[pdoStatement]] to be null. + */ + public function cancel() + { + $this->pdoStatement = null; + } + + /** + * Binds a parameter to the SQL statement to be executed. + * @param string|integer $name parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form `:name`. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $value Name of the PHP variable to bind to the SQL statement parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @param integer $length length of the data type + * @param mixed $driverOptions the driver-specific options + * @return static the current command being executed + * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php + */ + public function bindParam($name, &$value, $dataType = null, $length = null, $driverOptions = null) + { + $this->prepare(); + if ($dataType === null) { + $dataType = $this->db->getSchema()->getPdoType($value); + } + if ($length === null) { + $this->pdoStatement->bindParam($name, $value, $dataType); + } elseif ($driverOptions === null) { + $this->pdoStatement->bindParam($name, $value, $dataType, $length); + } else { + $this->pdoStatement->bindParam($name, $value, $dataType, $length, $driverOptions); + } + $this->params[$name] =& $value; + + return $this; + } + + /** + * Binds a value to a parameter. + * @param string|integer $name Parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form `:name`. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $value The value to bind to the parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @return static the current command being executed + * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php + */ + public function bindValue($name, $value, $dataType = null) + { + $this->prepare(); + if ($dataType === null) { + $dataType = $this->db->getSchema()->getPdoType($value); + } + $this->pdoStatement->bindValue($name, $value, $dataType); + $this->params[$name] = $value; + + return $this; + } + + /** + * Binds a list of values to the corresponding parameters. + * This is similar to [[bindValue()]] except that it binds multiple values at a time. + * Note that the SQL data type of each value is determined by its PHP type. + * @param array $values the values to be bound. This must be given in terms of an associative + * array with array keys being the parameter names, and array values the corresponding parameter values, + * e.g. `[':name' => 'John', ':age' => 25]`. By default, the PDO type of each value is determined + * by its PHP type. You may explicitly specify the PDO type by using an array: `[value, type]`, + * e.g. `[':name' => 'John', ':profile' => [$profile, \PDO::PARAM_LOB]]`. + * @return static the current command being executed + */ + public function bindValues($values) + { + if (!empty($values)) { + $this->prepare(); + foreach ($values as $name => $value) { + if (is_array($value)) { + $type = $value[1]; + $value = $value[0]; + } else { + $type = $this->db->getSchema()->getPdoType($value); + } + $this->pdoStatement->bindValue($name, $value, $type); + $this->params[$name] = $value; + } + } + + return $this; + } + + /** + * Executes the SQL statement. + * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. + * No result set will be returned. + * @return integer number of rows affected by the execution. + * @throws Exception execution failed + */ + public function execute() + { + $sql = $this->getSql(); + + $rawSql = $this->getRawSql(); + + Yii::info($rawSql, __METHOD__); + + if ($sql == '') { + return 0; + } + + $token = $rawSql; + try { + Yii::beginProfile($token, __METHOD__); + + $this->prepare(); + $this->pdoStatement->execute(); + $n = $this->pdoStatement->rowCount(); + + Yii::endProfile($token, __METHOD__); + + return $n; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + if ($e instanceof Exception) { + throw $e; + } else { + $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; + $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; + throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); + } + } + } + + /** + * Executes the SQL statement and returns query result. + * This method is for executing a SQL query that returns result set, such as `SELECT`. + * @return DataReader the reader object for fetching the query result + * @throws Exception execution failed + */ + public function query() + { + return $this->queryInternal(''); + } + + /** + * Executes the SQL statement and returns ALL rows at once. + * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. + * @return array all rows of the query result. Each array element is an array representing a row of data. + * An empty array is returned if the query results in nothing. + * @throws Exception execution failed + */ + public function queryAll($fetchMode = null) + { + return $this->queryInternal('fetchAll', $fetchMode); + } + + /** + * Executes the SQL statement and returns the first row of the result. + * This method is best used when only the first row of result is needed for a query. + * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + * @throws Exception execution failed + */ + public function queryOne($fetchMode = null) + { + return $this->queryInternal('fetch', $fetchMode); + } + + /** + * Executes the SQL statement and returns the value of the first column in the first row of data. + * This method is best used when only a single value is needed for a query. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if there is no value. + * @throws Exception execution failed + */ + public function queryScalar() + { + $result = $this->queryInternal('fetchColumn', 0); + if (is_resource($result) && get_resource_type($result) === 'stream') { + return stream_get_contents($result); + } else { + return $result; + } + } + + /** + * Executes the SQL statement and returns the first column of the result. + * This method is best used when only the first column of result (i.e. the first element in each row) + * is needed for a query. + * @return array the first column of the query result. Empty array is returned if the query results in nothing. + * @throws Exception execution failed + */ + public function queryColumn() + { + return $this->queryInternal('fetchAll', \PDO::FETCH_COLUMN); + } + + /** + * Performs the actual DB query of a SQL statement. + * @param string $method method of PDOStatement to be called + * @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. + * @return mixed the method execution result + * @throws Exception if the query causes any problem + */ + private function queryInternal($method, $fetchMode = null) + { + $db = $this->db; + $rawSql = $this->getRawSql(); + + Yii::info($rawSql, 'yii\db\Command::query'); + + /** @var \yii\caching\Cache $cache */ + if ($db->enableQueryCache && $method !== '') { + $cache = is_string($db->queryCache) ? Yii::$app->getComponent($db->queryCache) : $db->queryCache; + } + + if (isset($cache) && $cache instanceof Cache) { + $cacheKey = [ + __CLASS__, + $method, + $db->dsn, + $db->username, + $rawSql, + ]; + if (($result = $cache->get($cacheKey)) !== false) { + Yii::trace('Query result served from cache', 'yii\db\Command::query'); + + return $result; + } + } + + $token = $rawSql; + try { + Yii::beginProfile($token, 'yii\db\Command::query'); + + $this->prepare(); + $this->pdoStatement->execute(); + + if ($method === '') { + $result = new DataReader($this); + } else { + if ($fetchMode === null) { + $fetchMode = $this->fetchMode; + } + $result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode); + $this->pdoStatement->closeCursor(); + } + + Yii::endProfile($token, 'yii\db\Command::query'); + + if (isset($cache, $cacheKey) && $cache instanceof Cache) { + $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); + Yii::trace('Saved query result in cache', 'yii\db\Command::query'); + } + + return $result; + } catch (\Exception $e) { + Yii::endProfile($token, 'yii\db\Command::query'); + if ($e instanceof Exception) { + throw $e; + } else { + $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; + $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; + throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); + } + } + } + + /** + * Creates an INSERT command. + * For example, + * + * ~~~ + * $connection->createCommand()->insert('tbl_user', [ + * 'name' => 'Sam', + * 'age' => 30, + * ])->execute(); + * ~~~ + * + * The method will properly escape the column names, and bind the values to be inserted. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name => value) to be inserted into the table. + * @return Command the command object itself + */ + public function insert($table, $columns) + { + $params = []; + $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a batch INSERT command. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ])->execute(); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return Command the command object itself + */ + public function batchInsert($table, $columns, $rows) + { + $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows); + + return $this->setSql($sql); + } + + /** + * Creates an UPDATE command. + * For example, + * + * ~~~ + * $connection->createCommand()->update('tbl_user', ['status' => 1], 'age > 30')->execute(); + * ~~~ + * + * The method will properly escape the column names and bind the values to be updated. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $table the table to be updated. + * @param array $columns the column data (name => value) to be updated. + * @param string|array $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the parameters to be bound to the command + * @return Command the command object itself + */ + public function update($table, $columns, $condition = '', $params = []) + { + $sql = $this->db->getQueryBuilder()->update($table, $columns, $condition, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a DELETE command. + * For example, + * + * ~~~ + * $connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); + * ~~~ + * + * The method will properly escape the table and column names. + * + * Note that the created command is not executed until [[execute()]] is called. + * + * @param string $table the table where the data will be deleted from. + * @param string|array $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the parameters to be bound to the command + * @return Command the command object itself + */ + public function delete($table, $condition = '', $params = []) + { + $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params); + + return $this->setSql($sql)->bindValues($params); + } + + /** + * Creates a SQL command for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The method [[QueryBuilder::getColumnType()]] will be called + * to convert the abstract column types to physical ones. For example, `string` will be converted + * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name => definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + * @return Command the command object itself + */ + public function createTable($table, $columns, $options = null) + { + $sql = $this->db->getQueryBuilder()->createTable($table, $columns, $options); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function renameTable($table, $newName) + { + $sql = $this->db->getQueryBuilder()->renameTable($table, $newName); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropTable($table) + { + $sql = $this->db->getQueryBuilder()->dropTable($table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function truncateTable($table) + { + $sql = $this->db->getQueryBuilder()->truncateTable($table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. [[\yii\db\QueryBuilder::getColumnType()]] will be called + * to convert the give column type to the physical one. For example, `string` will be converted + * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. + * @return Command the command object itself + */ + public function addColumn($table, $column, $type) + { + $sql = $this->db->getQueryBuilder()->addColumn($table, $column, $type); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropColumn($table, $column) + { + $sql = $this->db->getQueryBuilder()->dropColumn($table, $column); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $oldName the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function renameColumn($table, $oldName, $newName) + { + $sql = $this->db->getQueryBuilder()->renameColumn($table, $oldName, $newName); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the column type. [[\yii\db\QueryBuilder::getColumnType()]] will be called + * to convert the give column type to the physical one. For example, `string` will be converted + * as `varchar(255)`, and `string not null` becomes `varchar(255) not null`. + * @return Command the command object itself + */ + public function alterColumn($table, $column, $type) + { + $sql = $this->db->getQueryBuilder()->alterColumn($table, $column, $type); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a primary key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * @return Command the command object itself. + */ + public function addPrimaryKey($name, $table, $columns) + { + $sql = $this->db->getQueryBuilder()->addPrimaryKey($name, $table, $columns); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return Command the command object itself + */ + public function dropPrimaryKey($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropPrimaryKey($name, $table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return Command the command object itself + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + $sql = $this->db->getQueryBuilder()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropForeignKey($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropForeignKey($name, $table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + * @return Command the command object itself + */ + public function createIndex($name, $table, $columns, $unique = false) + { + $sql = $this->db->getQueryBuilder()->createIndex($name, $table, $columns, $unique); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return Command the command object itself + */ + public function dropIndex($name, $table) + { + $sql = $this->db->getQueryBuilder()->dropIndex($name, $table); + + return $this->setSql($sql); + } + + /** + * Creates a SQL command for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $table the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return Command the command object itself + * @throws NotSupportedException if this is not supported by the underlying DBMS + */ + public function resetSequence($table, $value = null) + { + $sql = $this->db->getQueryBuilder()->resetSequence($table, $value); + + return $this->setSql($sql); + } + + /** + * Builds a SQL command for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema name of the tables. Defaults to empty string, meaning the current + * or default schema. + * @param string $table the table name. + * @return Command the command object itself + * @throws NotSupportedException if this is not supported by the underlying DBMS + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + $sql = $this->db->getQueryBuilder()->checkIntegrity($check, $schema, $table); + + return $this->setSql($sql); + } } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 0580da119fe..6610363cb12 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -67,7 +67,7 @@ * $connection->createCommand($sql2)->execute(); * // ... executing other SQL statements ... * $transaction->commit(); - * } catch(Exception $e) { + * } catch (Exception $e) { * $transaction->rollBack(); * } * ~~~ @@ -105,442 +105,445 @@ */ class Connection extends Component { - /** - * @event Event an event that is triggered after a DB connection is established - */ - const EVENT_AFTER_OPEN = 'afterOpen'; + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; - /** - * @var string the Data Source Name, or DSN, contains the information required to connect to the database. - * Please refer to the [PHP manual](http://www.php.net/manual/en/function.PDO-construct.php) on - * the format of the DSN string. - * @see charset - */ - public $dsn; - /** - * @var string the username for establishing DB connection. Defaults to `null` meaning no username to use. - */ - public $username; - /** - * @var string the password for establishing DB connection. Defaults to `null` meaning no password to use. - */ - public $password; - /** - * @var array PDO attributes (name => value) that should be set when calling [[open()]] - * to establish a DB connection. Please refer to the - * [PHP manual](http://www.php.net/manual/en/function.PDO-setAttribute.php) for - * details about available attributes. - */ - public $attributes; - /** - * @var PDO the PHP PDO instance associated with this DB connection. - * This property is mainly managed by [[open()]] and [[close()]] methods. - * When a DB connection is active, this property will represent a PDO instance; - * otherwise, it will be null. - */ - public $pdo; - /** - * @var boolean whether to enable schema caching. - * Note that in order to enable truly schema caching, a valid cache component as specified - * by [[schemaCache]] must be enabled and [[enableSchemaCache]] must be set true. - * @see schemaCacheDuration - * @see schemaCacheExclude - * @see schemaCache - */ - public $enableSchemaCache = false; - /** - * @var integer number of seconds that table metadata can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - * @see enableSchemaCache - */ - public $schemaCacheDuration = 3600; - /** - * @var array list of tables whose metadata should NOT be cached. Defaults to empty array. - * The table names may contain schema prefix, if any. Do not quote the table names. - * @see enableSchemaCache - */ - public $schemaCacheExclude = []; - /** - * @var Cache|string the cache object or the ID of the cache application component that - * is used to cache the table metadata. - * @see enableSchemaCache - */ - public $schemaCache = 'cache'; - /** - * @var boolean whether to enable query caching. - * Note that in order to enable query caching, a valid cache component as specified - * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true. - * - * Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on - * and off query caching on the fly. - * @see queryCacheDuration - * @see queryCache - * @see queryCacheDependency - * @see beginCache() - * @see endCache() - */ - public $enableQueryCache = false; - /** - * @var integer number of seconds that query results can remain valid in cache. - * Defaults to 3600, meaning 3600 seconds, or one hour. - * Use 0 to indicate that the cached data will never expire. - * @see enableQueryCache - */ - public $queryCacheDuration = 3600; - /** - * @var \yii\caching\Dependency the dependency that will be used when saving query results into cache. - * Defaults to null, meaning no dependency. - * @see enableQueryCache - */ - public $queryCacheDependency; - /** - * @var Cache|string the cache object or the ID of the cache application component - * that is used for query caching. - * @see enableQueryCache - */ - public $queryCache = 'cache'; - /** - * @var string the charset used for database connection. The property is only used - * for MySQL, PostgreSQL and CUBRID databases. Defaults to null, meaning using default charset - * as specified by the database. - * - * Note that if you're using GBK or BIG5 then it's highly recommended to - * specify charset via DSN like 'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'. - */ - public $charset; - /** - * @var boolean whether to turn on prepare emulation. Defaults to false, meaning PDO - * will use the native prepare support if available. For some databases (such as MySQL), - * this may need to be set true so that PDO can emulate the prepare support to bypass - * the buggy native prepare support. - * The default value is null, which means the PDO ATTR_EMULATE_PREPARES value will not be changed. - */ - public $emulatePrepare; - /** - * @var string the common prefix or suffix for table names. If a table name is given - * as `{{%TableName}}`, then the percentage character `%` will be replaced with this - * property value. For example, `{{%post}}` becomes `{{tbl_post}}`. - */ - public $tablePrefix = 'tbl_'; - /** - * @var array mapping between PDO driver names and [[Schema]] classes. - * The keys of the array are PDO driver names while the values the corresponding - * schema class name or configuration. Please refer to [[Yii::createObject()]] for - * details on how to specify a configuration. - * - * This property is mainly used by [[getSchema()]] when fetching the database schema information. - * You normally do not need to set this property unless you want to use your own - * [[Schema]] class to support DBMS that is not supported by Yii. - */ - public $schemaMap = [ - 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL - 'mysqli' => 'yii\db\mysql\Schema', // MySQL - 'mysql' => 'yii\db\mysql\Schema', // MySQL - 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 - 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 - 'sqlsrv' => 'yii\db\mssql\Schema', // newer MSSQL driver on MS Windows hosts - 'oci' => 'yii\db\oci\Schema', // Oracle driver - 'mssql' => 'yii\db\mssql\Schema', // older MSSQL driver on MS Windows hosts - 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on GNU/Linux (and maybe other OSes) hosts - 'cubrid' => 'yii\db\cubrid\Schema', // CUBRID - ]; - /** - * @var string Custom PDO wrapper class. If not set, it will use "PDO" or "yii\db\mssql\PDO" when MSSQL is used. - */ - public $pdoClass; - /** - * @var boolean whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint). - * Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect. - */ - public $enableSavepoint = true; - /** - * @var Transaction the currently active transaction - */ - private $_transaction; - /** - * @var Schema the database schema - */ - private $_schema; + /** + * @var string the Data Source Name, or DSN, contains the information required to connect to the database. + * Please refer to the [PHP manual](http://www.php.net/manual/en/function.PDO-construct.php) on + * the format of the DSN string. + * @see charset + */ + public $dsn; + /** + * @var string the username for establishing DB connection. Defaults to `null` meaning no username to use. + */ + public $username; + /** + * @var string the password for establishing DB connection. Defaults to `null` meaning no password to use. + */ + public $password; + /** + * @var array PDO attributes (name => value) that should be set when calling [[open()]] + * to establish a DB connection. Please refer to the + * [PHP manual](http://www.php.net/manual/en/function.PDO-setAttribute.php) for + * details about available attributes. + */ + public $attributes; + /** + * @var PDO the PHP PDO instance associated with this DB connection. + * This property is mainly managed by [[open()]] and [[close()]] methods. + * When a DB connection is active, this property will represent a PDO instance; + * otherwise, it will be null. + */ + public $pdo; + /** + * @var boolean whether to enable schema caching. + * Note that in order to enable truly schema caching, a valid cache component as specified + * by [[schemaCache]] must be enabled and [[enableSchemaCache]] must be set true. + * @see schemaCacheDuration + * @see schemaCacheExclude + * @see schemaCache + */ + public $enableSchemaCache = false; + /** + * @var integer number of seconds that table metadata can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + * @see enableSchemaCache + */ + public $schemaCacheDuration = 3600; + /** + * @var array list of tables whose metadata should NOT be cached. Defaults to empty array. + * The table names may contain schema prefix, if any. Do not quote the table names. + * @see enableSchemaCache + */ + public $schemaCacheExclude = []; + /** + * @var Cache|string the cache object or the ID of the cache application component that + * is used to cache the table metadata. + * @see enableSchemaCache + */ + public $schemaCache = 'cache'; + /** + * @var boolean whether to enable query caching. + * Note that in order to enable query caching, a valid cache component as specified + * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true. + * + * Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on + * and off query caching on the fly. + * @see queryCacheDuration + * @see queryCache + * @see queryCacheDependency + * @see beginCache() + * @see endCache() + */ + public $enableQueryCache = false; + /** + * @var integer number of seconds that query results can remain valid in cache. + * Defaults to 3600, meaning 3600 seconds, or one hour. + * Use 0 to indicate that the cached data will never expire. + * @see enableQueryCache + */ + public $queryCacheDuration = 3600; + /** + * @var \yii\caching\Dependency the dependency that will be used when saving query results into cache. + * Defaults to null, meaning no dependency. + * @see enableQueryCache + */ + public $queryCacheDependency; + /** + * @var Cache|string the cache object or the ID of the cache application component + * that is used for query caching. + * @see enableQueryCache + */ + public $queryCache = 'cache'; + /** + * @var string the charset used for database connection. The property is only used + * for MySQL, PostgreSQL and CUBRID databases. Defaults to null, meaning using default charset + * as specified by the database. + * + * Note that if you're using GBK or BIG5 then it's highly recommended to + * specify charset via DSN like 'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'. + */ + public $charset; + /** + * @var boolean whether to turn on prepare emulation. Defaults to false, meaning PDO + * will use the native prepare support if available. For some databases (such as MySQL), + * this may need to be set true so that PDO can emulate the prepare support to bypass + * the buggy native prepare support. + * The default value is null, which means the PDO ATTR_EMULATE_PREPARES value will not be changed. + */ + public $emulatePrepare; + /** + * @var string the common prefix or suffix for table names. If a table name is given + * as `{{%TableName}}`, then the percentage character `%` will be replaced with this + * property value. For example, `{{%post}}` becomes `{{tbl_post}}`. + */ + public $tablePrefix = 'tbl_'; + /** + * @var array mapping between PDO driver names and [[Schema]] classes. + * The keys of the array are PDO driver names while the values the corresponding + * schema class name or configuration. Please refer to [[Yii::createObject()]] for + * details on how to specify a configuration. + * + * This property is mainly used by [[getSchema()]] when fetching the database schema information. + * You normally do not need to set this property unless you want to use your own + * [[Schema]] class to support DBMS that is not supported by Yii. + */ + public $schemaMap = [ + 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL + 'mysqli' => 'yii\db\mysql\Schema', // MySQL + 'mysql' => 'yii\db\mysql\Schema', // MySQL + 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 + 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 + 'sqlsrv' => 'yii\db\mssql\Schema', // newer MSSQL driver on MS Windows hosts + 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'mssql' => 'yii\db\mssql\Schema', // older MSSQL driver on MS Windows hosts + 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on GNU/Linux (and maybe other OSes) hosts + 'cubrid' => 'yii\db\cubrid\Schema', // CUBRID + ]; + /** + * @var string Custom PDO wrapper class. If not set, it will use "PDO" or "yii\db\mssql\PDO" when MSSQL is used. + */ + public $pdoClass; + /** + * @var boolean whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint). + * Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect. + */ + public $enableSavepoint = true; + /** + * @var Transaction the currently active transaction + */ + private $_transaction; + /** + * @var Schema the database schema + */ + private $_schema; + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return $this->pdo !== null; + } - /** - * Returns a value indicating whether the DB connection is established. - * @return boolean whether the DB connection is established - */ - public function getIsActive() - { - return $this->pdo !== null; - } + /** + * Turns on query caching. + * This method is provided as a shortcut to setting two properties that are related + * with query caching: [[queryCacheDuration]] and [[queryCacheDependency]]. + * @param integer $duration the number of seconds that query results may remain valid in cache. + * If not set, it will use the value of [[queryCacheDuration]]. See [[queryCacheDuration]] for more details. + * @param \yii\caching\Dependency $dependency the dependency for the cached query result. + * See [[queryCacheDependency]] for more details. + */ + public function beginCache($duration = null, $dependency = null) + { + $this->enableQueryCache = true; + if ($duration !== null) { + $this->queryCacheDuration = $duration; + } + $this->queryCacheDependency = $dependency; + } - /** - * Turns on query caching. - * This method is provided as a shortcut to setting two properties that are related - * with query caching: [[queryCacheDuration]] and [[queryCacheDependency]]. - * @param integer $duration the number of seconds that query results may remain valid in cache. - * If not set, it will use the value of [[queryCacheDuration]]. See [[queryCacheDuration]] for more details. - * @param \yii\caching\Dependency $dependency the dependency for the cached query result. - * See [[queryCacheDependency]] for more details. - */ - public function beginCache($duration = null, $dependency = null) - { - $this->enableQueryCache = true; - if ($duration !== null) { - $this->queryCacheDuration = $duration; - } - $this->queryCacheDependency = $dependency; - } + /** + * Turns off query caching. + */ + public function endCache() + { + $this->enableQueryCache = false; + } - /** - * Turns off query caching. - */ - public function endCache() - { - $this->enableQueryCache = false; - } + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + if ($this->pdo === null) { + if (empty($this->dsn)) { + throw new InvalidConfigException('Connection::dsn cannot be empty.'); + } + $token = 'Opening DB connection: ' . $this->dsn; + try { + Yii::trace($token, __METHOD__); + Yii::beginProfile($token, __METHOD__); + $this->pdo = $this->createPdoInstance(); + $this->initConnection(); + Yii::endProfile($token, __METHOD__); + } catch (\PDOException $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), $e->errorInfo, (int) $e->getCode(), $e); + } + } + } - /** - * Establishes a DB connection. - * It does nothing if a DB connection has already been established. - * @throws Exception if connection fails - */ - public function open() - { - if ($this->pdo === null) { - if (empty($this->dsn)) { - throw new InvalidConfigException('Connection::dsn cannot be empty.'); - } - $token = 'Opening DB connection: ' . $this->dsn; - try { - Yii::trace($token, __METHOD__); - Yii::beginProfile($token, __METHOD__); - $this->pdo = $this->createPdoInstance(); - $this->initConnection(); - Yii::endProfile($token, __METHOD__); - } catch (\PDOException $e) { - Yii::endProfile($token, __METHOD__); - throw new Exception($e->getMessage(), $e->errorInfo, (int)$e->getCode(), $e); - } - } - } + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + if ($this->pdo !== null) { + Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); + $this->pdo = null; + $this->_schema = null; + $this->_transaction = null; + } + } - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - if ($this->pdo !== null) { - Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); - $this->pdo = null; - $this->_schema = null; - $this->_transaction = null; - } - } + /** + * Creates the PDO instance. + * This method is called by [[open]] to establish a DB connection. + * The default implementation will create a PHP PDO instance. + * You may override this method if the default PDO needs to be adapted for certain DBMS. + * @return PDO the pdo instance + */ + protected function createPdoInstance() + { + $pdoClass = $this->pdoClass; + if ($pdoClass === null) { + $pdoClass = 'PDO'; + if (($pos = strpos($this->dsn, ':')) !== false) { + $driver = strtolower(substr($this->dsn, 0, $pos)); + if ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv') { + $pdoClass = 'yii\db\mssql\PDO'; + } + } + } - /** - * Creates the PDO instance. - * This method is called by [[open]] to establish a DB connection. - * The default implementation will create a PHP PDO instance. - * You may override this method if the default PDO needs to be adapted for certain DBMS. - * @return PDO the pdo instance - */ - protected function createPdoInstance() - { - $pdoClass = $this->pdoClass; - if ($pdoClass === null) { - $pdoClass = 'PDO'; - if (($pos = strpos($this->dsn, ':')) !== false) { - $driver = strtolower(substr($this->dsn, 0, $pos)); - if ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv') { - $pdoClass = 'yii\db\mssql\PDO'; - } - } - } + return new $pdoClass($this->dsn, $this->username, $this->password, $this->attributes); + } - return new $pdoClass($this->dsn, $this->username, $this->password, $this->attributes); - } + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES` + * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty. + * It then triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) { + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); + } + if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'])) { + $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset)); + } + $this->trigger(self::EVENT_AFTER_OPEN); + } - /** - * Initializes the DB connection. - * This method is invoked right after the DB connection is established. - * The default implementation turns on `PDO::ATTR_EMULATE_PREPARES` - * if [[emulatePrepare]] is true, and sets the database [[charset]] if it is not empty. - * It then triggers an [[EVENT_AFTER_OPEN]] event. - */ - protected function initConnection() - { - $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - if ($this->emulatePrepare !== null && constant('PDO::ATTR_EMULATE_PREPARES')) { - $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->emulatePrepare); - } - if ($this->charset !== null && in_array($this->getDriverName(), ['pgsql', 'mysql', 'mysqli', 'cubrid'])) { - $this->pdo->exec('SET NAMES ' . $this->pdo->quote($this->charset)); - } - $this->trigger(self::EVENT_AFTER_OPEN); - } + /** + * Creates a command for execution. + * @param string $sql the SQL statement to be executed + * @param array $params the parameters to be bound to the SQL statement + * @return Command the DB command + */ + public function createCommand($sql = null, $params = []) + { + $this->open(); + $command = new Command([ + 'db' => $this, + 'sql' => $sql, + ]); - /** - * Creates a command for execution. - * @param string $sql the SQL statement to be executed - * @param array $params the parameters to be bound to the SQL statement - * @return Command the DB command - */ - public function createCommand($sql = null, $params = []) - { - $this->open(); - $command = new Command([ - 'db' => $this, - 'sql' => $sql, - ]); - return $command->bindValues($params); - } + return $command->bindValues($params); + } - /** - * Returns the currently active transaction. - * @return Transaction the currently active transaction. Null if no active transaction. - */ - public function getTransaction() - { - return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null; - } + /** + * Returns the currently active transaction. + * @return Transaction the currently active transaction. Null if no active transaction. + */ + public function getTransaction() + { + return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null; + } - /** - * Starts a transaction. - * @return Transaction the transaction initiated - */ - public function beginTransaction() - { - $this->open(); + /** + * Starts a transaction. + * @return Transaction the transaction initiated + */ + public function beginTransaction() + { + $this->open(); - if (($transaction = $this->getTransaction()) === null) { - $transaction = $this->_transaction = new Transaction(['db' => $this]); - } - $transaction->begin(); - return $transaction; - } + if (($transaction = $this->getTransaction()) === null) { + $transaction = $this->_transaction = new Transaction(['db' => $this]); + } + $transaction->begin(); - /** - * Returns the schema information for the database opened by this connection. - * @return Schema the schema information for the database opened by this connection. - * @throws NotSupportedException if there is no support for the current driver type - */ - public function getSchema() - { - if ($this->_schema !== null) { - return $this->_schema; - } else { - $driver = $this->getDriverName(); - if (isset($this->schemaMap[$driver])) { - $config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver]; - $config['db'] = $this; - return $this->_schema = Yii::createObject($config); - } else { - throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS."); - } - } - } + return $transaction; + } - /** - * Returns the query builder for the current DB connection. - * @return QueryBuilder the query builder for the current DB connection. - */ - public function getQueryBuilder() - { - return $this->getSchema()->getQueryBuilder(); - } + /** + * Returns the schema information for the database opened by this connection. + * @return Schema the schema information for the database opened by this connection. + * @throws NotSupportedException if there is no support for the current driver type + */ + public function getSchema() + { + if ($this->_schema !== null) { + return $this->_schema; + } else { + $driver = $this->getDriverName(); + if (isset($this->schemaMap[$driver])) { + $config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver]; + $config['db'] = $this; - /** - * Obtains the schema information for the named table. - * @param string $name table name. - * @param boolean $refresh whether to reload the table schema even if it is found in the cache. - * @return TableSchema table schema information. Null if the named table does not exist. - */ - public function getTableSchema($name, $refresh = false) - { - return $this->getSchema()->getTableSchema($name, $refresh); - } + return $this->_schema = Yii::createObject($config); + } else { + throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS."); + } + } + } - /** - * Returns the ID of the last inserted row or sequence value. - * @param string $sequenceName name of the sequence object (required by some DBMS) - * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object - * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php - */ - public function getLastInsertID($sequenceName = '') - { - return $this->getSchema()->getLastInsertID($sequenceName); - } + /** + * Returns the query builder for the current DB connection. + * @return QueryBuilder the query builder for the current DB connection. + */ + public function getQueryBuilder() + { + return $this->getSchema()->getQueryBuilder(); + } - /** - * Quotes a string value for use in a query. - * Note that if the parameter is not a string, it will be returned without change. - * @param string $str string to be quoted - * @return string the properly quoted string - * @see http://www.php.net/manual/en/function.PDO-quote.php - */ - public function quoteValue($str) - { - return $this->getSchema()->quoteValue($str); - } + /** + * Obtains the schema information for the named table. + * @param string $name table name. + * @param boolean $refresh whether to reload the table schema even if it is found in the cache. + * @return TableSchema table schema information. Null if the named table does not exist. + */ + public function getTableSchema($name, $refresh = false) + { + return $this->getSchema()->getTableSchema($name, $refresh); + } - /** - * Quotes a table name for use in a query. - * If the table name contains schema prefix, the prefix will also be properly quoted. - * If the table name is already quoted or contains special characters including '(', '[[' and '{{', - * then this method will do nothing. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - return $this->getSchema()->quoteTableName($name); - } + /** + * Returns the ID of the last inserted row or sequence value. + * @param string $sequenceName name of the sequence object (required by some DBMS) + * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object + * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php + */ + public function getLastInsertID($sequenceName = '') + { + return $this->getSchema()->getLastInsertID($sequenceName); + } - /** - * Quotes a column name for use in a query. - * If the column name contains prefix, the prefix will also be properly quoted. - * If the column name is already quoted or contains special characters including '(', '[[' and '{{', - * then this method will do nothing. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteColumnName($name) - { - return $this->getSchema()->quoteColumnName($name); - } + /** + * Quotes a string value for use in a query. + * Note that if the parameter is not a string, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + return $this->getSchema()->quoteValue($str); + } - /** - * Processes a SQL statement by quoting table and column names that are enclosed within double brackets. - * Tokens enclosed within double curly brackets are treated as table names, while - * tokens enclosed within double square brackets are column names. They will be quoted accordingly. - * Also, the percentage character "%" at the beginning or ending of a table name will be replaced - * with [[tablePrefix]]. - * @param string $sql the SQL to be quoted - * @return string the quoted SQL - */ - public function quoteSql($sql) - { - return preg_replace_callback('/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', - function ($matches) { - if (isset($matches[3])) { - return $this->quoteColumnName($matches[3]); - } else { - return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2])); - } - }, $sql); - } + /** + * Quotes a table name for use in a query. + * If the table name contains schema prefix, the prefix will also be properly quoted. + * If the table name is already quoted or contains special characters including '(', '[[' and '{{', + * then this method will do nothing. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return $this->getSchema()->quoteTableName($name); + } - /** - * Returns the name of the DB driver for the current [[dsn]]. - * @return string name of the DB driver - */ - public function getDriverName() - { - if (($pos = strpos($this->dsn, ':')) !== false) { - return strtolower(substr($this->dsn, 0, $pos)); - } else { - $this->open(); - return strtolower($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); - } - } + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * If the column name is already quoted or contains special characters including '(', '[[' and '{{', + * then this method will do nothing. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteColumnName($name) + { + return $this->getSchema()->quoteColumnName($name); + } + + /** + * Processes a SQL statement by quoting table and column names that are enclosed within double brackets. + * Tokens enclosed within double curly brackets are treated as table names, while + * tokens enclosed within double square brackets are column names. They will be quoted accordingly. + * Also, the percentage character "%" at the beginning or ending of a table name will be replaced + * with [[tablePrefix]]. + * @param string $sql the SQL to be quoted + * @return string the quoted SQL + */ + public function quoteSql($sql) + { + return preg_replace_callback('/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + function ($matches) { + if (isset($matches[3])) { + return $this->quoteColumnName($matches[3]); + } else { + return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2])); + } + }, $sql); + } + + /** + * Returns the name of the DB driver for the current [[dsn]]. + * @return string name of the DB driver + */ + public function getDriverName() + { + if (($pos = strpos($this->dsn, ':')) !== false) { + return strtolower(substr($this->dsn, 0, $pos)); + } else { + $this->open(); + + return strtolower($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); + } + } } diff --git a/framework/db/DataReader.php b/framework/db/DataReader.php index 213db52b6e2..bec616c9c5a 100644 --- a/framework/db/DataReader.php +++ b/framework/db/DataReader.php @@ -49,216 +49,217 @@ */ class DataReader extends \yii\base\Object implements \Iterator, \Countable { - /** - * @var \PDOStatement the PDOStatement associated with the command - */ - private $_statement; - private $_closed = false; - private $_row; - private $_index = -1; + /** + * @var \PDOStatement the PDOStatement associated with the command + */ + private $_statement; + private $_closed = false; + private $_row; + private $_index = -1; - /** - * Constructor. - * @param Command $command the command generating the query result - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct(Command $command, $config = []) - { - $this->_statement = $command->pdoStatement; - $this->_statement->setFetchMode(\PDO::FETCH_ASSOC); - parent::__construct($config); - } + /** + * Constructor. + * @param Command $command the command generating the query result + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct(Command $command, $config = []) + { + $this->_statement = $command->pdoStatement; + $this->_statement->setFetchMode(\PDO::FETCH_ASSOC); + parent::__construct($config); + } - /** - * Binds a column to a PHP variable. - * When rows of data are being fetched, the corresponding column value - * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND. - * @param integer|string $column Number of the column (1-indexed) or name of the column - * in the result set. If using the column name, be aware that the name - * should match the case of the column, as returned by the driver. - * @param mixed $value Name of the PHP variable to which the column will be bound. - * @param integer $dataType Data type of the parameter - * @see http://www.php.net/manual/en/function.PDOStatement-bindColumn.php - */ - public function bindColumn($column, &$value, $dataType = null) - { - if ($dataType === null) { - $this->_statement->bindColumn($column, $value); - } else { - $this->_statement->bindColumn($column, $value, $dataType); - } - } + /** + * Binds a column to a PHP variable. + * When rows of data are being fetched, the corresponding column value + * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND. + * @param integer|string $column Number of the column (1-indexed) or name of the column + * in the result set. If using the column name, be aware that the name + * should match the case of the column, as returned by the driver. + * @param mixed $value Name of the PHP variable to which the column will be bound. + * @param integer $dataType Data type of the parameter + * @see http://www.php.net/manual/en/function.PDOStatement-bindColumn.php + */ + public function bindColumn($column, &$value, $dataType = null) + { + if ($dataType === null) { + $this->_statement->bindColumn($column, $value); + } else { + $this->_statement->bindColumn($column, $value, $dataType); + } + } - /** - * Set the default fetch mode for this statement - * @param integer $mode fetch mode - * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php - */ - public function setFetchMode($mode) - { - $params = func_get_args(); - call_user_func_array([$this->_statement, 'setFetchMode'], $params); - } + /** + * Set the default fetch mode for this statement + * @param integer $mode fetch mode + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + */ + public function setFetchMode($mode) + { + $params = func_get_args(); + call_user_func_array([$this->_statement, 'setFetchMode'], $params); + } - /** - * Advances the reader to the next row in a result set. - * @return array the current row, false if no more row available - */ - public function read() - { - return $this->_statement->fetch(); - } + /** + * Advances the reader to the next row in a result set. + * @return array the current row, false if no more row available + */ + public function read() + { + return $this->_statement->fetch(); + } - /** - * Returns a single column from the next row of a result set. - * @param integer $columnIndex zero-based column index - * @return mixed the column of the current row, false if no more rows available - */ - public function readColumn($columnIndex) - { - return $this->_statement->fetchColumn($columnIndex); - } + /** + * Returns a single column from the next row of a result set. + * @param integer $columnIndex zero-based column index + * @return mixed the column of the current row, false if no more rows available + */ + public function readColumn($columnIndex) + { + return $this->_statement->fetchColumn($columnIndex); + } - /** - * Returns an object populated with the next row of data. - * @param string $className class name of the object to be created and populated - * @param array $fields Elements of this array are passed to the constructor - * @return mixed the populated object, false if no more row of data available - */ - public function readObject($className, $fields) - { - return $this->_statement->fetchObject($className, $fields); - } + /** + * Returns an object populated with the next row of data. + * @param string $className class name of the object to be created and populated + * @param array $fields Elements of this array are passed to the constructor + * @return mixed the populated object, false if no more row of data available + */ + public function readObject($className, $fields) + { + return $this->_statement->fetchObject($className, $fields); + } - /** - * Reads the whole result set into an array. - * @return array the result set (each array element represents a row of data). - * An empty array will be returned if the result contains no row. - */ - public function readAll() - { - return $this->_statement->fetchAll(); - } + /** + * Reads the whole result set into an array. + * @return array the result set (each array element represents a row of data). + * An empty array will be returned if the result contains no row. + */ + public function readAll() + { + return $this->_statement->fetchAll(); + } - /** - * Advances the reader to the next result when reading the results of a batch of statements. - * This method is only useful when there are multiple result sets - * returned by the query. Not all DBMS support this feature. - * @return boolean Returns true on success or false on failure. - */ - public function nextResult() - { - if (($result = $this->_statement->nextRowset()) !== false) { - $this->_index = -1; - } - return $result; - } + /** + * Advances the reader to the next result when reading the results of a batch of statements. + * This method is only useful when there are multiple result sets + * returned by the query. Not all DBMS support this feature. + * @return boolean Returns true on success or false on failure. + */ + public function nextResult() + { + if (($result = $this->_statement->nextRowset()) !== false) { + $this->_index = -1; + } - /** - * Closes the reader. - * This frees up the resources allocated for executing this SQL statement. - * Read attempts after this method call are unpredictable. - */ - public function close() - { - $this->_statement->closeCursor(); - $this->_closed = true; - } + return $result; + } - /** - * whether the reader is closed or not. - * @return boolean whether the reader is closed or not. - */ - public function getIsClosed() - { - return $this->_closed; - } + /** + * Closes the reader. + * This frees up the resources allocated for executing this SQL statement. + * Read attempts after this method call are unpredictable. + */ + public function close() + { + $this->_statement->closeCursor(); + $this->_closed = true; + } - /** - * Returns the number of rows in the result set. - * Note, most DBMS may not give a meaningful count. - * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. - * @return integer number of rows contained in the result. - */ - public function getRowCount() - { - return $this->_statement->rowCount(); - } + /** + * whether the reader is closed or not. + * @return boolean whether the reader is closed or not. + */ + public function getIsClosed() + { + return $this->_closed; + } - /** - * Returns the number of rows in the result set. - * This method is required by the Countable interface. - * Note, most DBMS may not give a meaningful count. - * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. - * @return integer number of rows contained in the result. - */ - public function count() - { - return $this->getRowCount(); - } + /** + * Returns the number of rows in the result set. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function getRowCount() + { + return $this->_statement->rowCount(); + } - /** - * Returns the number of columns in the result set. - * Note, even there's no row in the reader, this still gives correct column number. - * @return integer the number of columns in the result set. - */ - public function getColumnCount() - { - return $this->_statement->columnCount(); - } + /** + * Returns the number of rows in the result set. + * This method is required by the Countable interface. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function count() + { + return $this->getRowCount(); + } - /** - * Resets the iterator to the initial state. - * This method is required by the interface Iterator. - * @throws InvalidCallException if this method is invoked twice - */ - public function rewind() - { - if ($this->_index < 0) { - $this->_row = $this->_statement->fetch(); - $this->_index = 0; - } else { - throw new InvalidCallException('DataReader cannot rewind. It is a forward-only reader.'); - } - } + /** + * Returns the number of columns in the result set. + * Note, even there's no row in the reader, this still gives correct column number. + * @return integer the number of columns in the result set. + */ + public function getColumnCount() + { + return $this->_statement->columnCount(); + } - /** - * Returns the index of the current row. - * This method is required by the interface Iterator. - * @return integer the index of the current row. - */ - public function key() - { - return $this->_index; - } + /** + * Resets the iterator to the initial state. + * This method is required by the interface Iterator. + * @throws InvalidCallException if this method is invoked twice + */ + public function rewind() + { + if ($this->_index < 0) { + $this->_row = $this->_statement->fetch(); + $this->_index = 0; + } else { + throw new InvalidCallException('DataReader cannot rewind. It is a forward-only reader.'); + } + } - /** - * Returns the current row. - * This method is required by the interface Iterator. - * @return mixed the current row. - */ - public function current() - { - return $this->_row; - } + /** + * Returns the index of the current row. + * This method is required by the interface Iterator. + * @return integer the index of the current row. + */ + public function key() + { + return $this->_index; + } - /** - * Moves the internal pointer to the next row. - * This method is required by the interface Iterator. - */ - public function next() - { - $this->_row = $this->_statement->fetch(); - $this->_index++; - } + /** + * Returns the current row. + * This method is required by the interface Iterator. + * @return mixed the current row. + */ + public function current() + { + return $this->_row; + } - /** - * Returns whether there is a row of data at current position. - * This method is required by the interface Iterator. - * @return boolean whether there is a row of data at current position. - */ - public function valid() - { - return $this->_row !== false; - } + /** + * Moves the internal pointer to the next row. + * This method is required by the interface Iterator. + */ + public function next() + { + $this->_row = $this->_statement->fetch(); + $this->_index++; + } + + /** + * Returns whether there is a row of data at current position. + * This method is required by the interface Iterator. + * @return boolean whether there is a row of data at current position. + */ + public function valid() + { + return $this->_row !== false; + } } diff --git a/framework/db/Exception.php b/framework/db/Exception.php index d5cafeff746..44949f65c02 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -15,36 +15,36 @@ */ class Exception extends \yii\base\Exception { - /** - * @var array the error info provided by a PDO exception. This is the same as returned - * by [PDO::errorInfo](http://www.php.net/manual/en/pdo.errorinfo.php). - */ - public $errorInfo = []; + /** + * @var array the error info provided by a PDO exception. This is the same as returned + * by [PDO::errorInfo](http://www.php.net/manual/en/pdo.errorinfo.php). + */ + public $errorInfo = []; - /** - * Constructor. - * @param string $message PDO error message - * @param array $errorInfo PDO error info - * @param integer $code PDO error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null) - { - $this->errorInfo = $errorInfo; - parent::__construct($message, $code, $previous); - } + /** + * Constructor. + * @param string $message PDO error message + * @param array $errorInfo PDO error info + * @param integer $code PDO error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null) + { + $this->errorInfo = $errorInfo; + parent::__construct($message, $code, $previous); + } - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Database Exception'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Database Exception'; + } - public function __toString() - { - return parent::__toString() . PHP_EOL - . 'Additional Information:' . PHP_EOL . print_r($this->errorInfo, true); - } + public function __toString() + { + return parent::__toString() . PHP_EOL + . 'Additional Information:' . PHP_EOL . print_r($this->errorInfo, true); + } } diff --git a/framework/db/Expression.php b/framework/db/Expression.php index 7fa9124d72c..0e2130e3326 100644 --- a/framework/db/Expression.php +++ b/framework/db/Expression.php @@ -25,36 +25,36 @@ */ class Expression extends \yii\base\Object { - /** - * @var string the DB expression - */ - public $expression; - /** - * @var array list of parameters that should be bound for this expression. - * The keys are placeholders appearing in [[expression]] and the values - * are the corresponding parameter values. - */ - public $params = []; + /** + * @var string the DB expression + */ + public $expression; + /** + * @var array list of parameters that should be bound for this expression. + * The keys are placeholders appearing in [[expression]] and the values + * are the corresponding parameter values. + */ + public $params = []; - /** - * Constructor. - * @param string $expression the DB expression - * @param array $params parameters - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($expression, $params = [], $config = []) - { - $this->expression = $expression; - $this->params = $params; - parent::__construct($config); - } + /** + * Constructor. + * @param string $expression the DB expression + * @param array $params parameters + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($expression, $params = [], $config = []) + { + $this->expression = $expression; + $this->params = $params; + parent::__construct($config); + } - /** - * String magic method - * @return string the DB expression - */ - public function __toString() - { - return $this->expression; - } + /** + * String magic method + * @return string the DB expression + */ + public function __toString() + { + return $this->expression; + } } diff --git a/framework/db/Migration.php b/framework/db/Migration.php index 26659d1d4d3..89927a7c166 100644 --- a/framework/db/Migration.php +++ b/framework/db/Migration.php @@ -35,381 +35,387 @@ */ class Migration extends \yii\base\Component { - /** - * @var Connection the database connection that this migration should work with. - * If not set, it will be initialized as the 'db' application component. - */ - public $db; - - /** - * Initializes the migration. - * This method will set [[db]] to be the 'db' application component, if it is null. - */ - public function init() - { - parent::init(); - if ($this->db === null) { - $this->db = \Yii::$app->getComponent('db'); - } - } - - /** - * This method contains the logic to be executed when applying this migration. - * Child classes may overwrite this method to provide actual migration logic. - * @return boolean return a false value to indicate the migration fails - * and should not proceed further. All other return values mean the migration succeeds. - */ - public function up() - { - $transaction = $this->db->beginTransaction(); - try { - if ($this->safeUp() === false) { - $transaction->rollBack(); - return false; - } - $transaction->commit(); - } catch (\Exception $e) { - echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; - echo $e->getTraceAsString() . "\n"; - $transaction->rollBack(); - return false; - } - return null; - } - - /** - * This method contains the logic to be executed when removing this migration. - * The default implementation throws an exception indicating the migration cannot be removed. - * Child classes may override this method if the corresponding migrations can be removed. - * @return boolean return a false value to indicate the migration fails - * and should not proceed further. All other return values mean the migration succeeds. - */ - public function down() - { - $transaction = $this->db->beginTransaction(); - try { - if ($this->safeDown() === false) { - $transaction->rollBack(); - return false; - } - $transaction->commit(); - } catch (\Exception $e) { - echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; - echo $e->getTraceAsString() . "\n"; - $transaction->rollBack(); - return false; - } - return null; - } - - /** - * This method contains the logic to be executed when applying this migration. - * This method differs from [[up()]] in that the DB logic implemented here will - * be enclosed within a DB transaction. - * Child classes may implement this method instead of [[up()]] if the DB logic - * needs to be within a transaction. - * @return boolean return a false value to indicate the migration fails - * and should not proceed further. All other return values mean the migration succeeds. - */ - public function safeUp() - { - } - - /** - * This method contains the logic to be executed when removing this migration. - * This method differs from [[down()]] in that the DB logic implemented here will - * be enclosed within a DB transaction. - * Child classes may implement this method instead of [[up()]] if the DB logic - * needs to be within a transaction. - * @return boolean return a false value to indicate the migration fails - * and should not proceed further. All other return values mean the migration succeeds. - */ - public function safeDown() - { - } - - /** - * Executes a SQL statement. - * This method executes the specified SQL statement using [[db]]. - * @param string $sql the SQL statement to be executed - * @param array $params input parameters (name => value) for the SQL execution. - * See [[Command::execute()]] for more details. - */ - public function execute($sql, $params = []) - { - echo " > execute SQL: $sql ..."; - $time = microtime(true); - $this->db->createCommand($sql)->bindValues($params)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Creates and executes an INSERT SQL statement. - * The method will properly escape the column names, and bind the values to be inserted. - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name => value) to be inserted into the table. - */ - public function insert($table, $columns) - { - echo " > insert into $table ..."; - $time = microtime(true); - $this->db->createCommand()->insert($table, $columns)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Creates and executes an batch INSERT SQL statement. - * The method will properly escape the column names, and bind the values to be inserted. - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column names. - * @param array $rows the rows to be batch inserted into the table - */ - public function batchInsert($table, $columns, $rows) - { - echo " > insert into $table ..."; - $time = microtime(true); - $this->db->createCommand()->batchInsert($table, $columns, $rows)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Creates and executes an UPDATE SQL statement. - * The method will properly escape the column names and bind the values to be updated. - * @param string $table the table to be updated. - * @param array $columns the column data (name => value) to be updated. - * @param array|string $condition the conditions that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify conditions. - * @param array $params the parameters to be bound to the query. - */ - public function update($table, $columns, $condition = '', $params = []) - { - echo " > update $table ..."; - $time = microtime(true); - $this->db->createCommand()->update($table, $columns, $condition, $params)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Creates and executes a DELETE SQL statement. - * @param string $table the table where the data will be deleted from. - * @param array|string $condition the conditions that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify conditions. - * @param array $params the parameters to be bound to the query. - */ - public function delete($table, $condition = '', $params = []) - { - echo " > delete from $table ..."; - $time = microtime(true); - $this->db->createCommand()->delete($table, $condition, $params)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for creating a new DB table. - * - * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), - * where name stands for a column name which will be properly quoted by the method, and definition - * stands for the column type which can contain an abstract DB type. - * - * The [[QueryBuilder::getColumnType()]] method will be invoked to convert any abstract type into a physical one. - * - * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly - * put into the generated SQL. - * - * @param string $table the name of the table to be created. The name will be properly quoted by the method. - * @param array $columns the columns (name => definition) in the new table. - * @param string $options additional SQL fragment that will be appended to the generated SQL. - */ - public function createTable($table, $columns, $options = null) - { - echo " > create table $table ..."; - $time = microtime(true); - $this->db->createCommand()->createTable($table, $columns, $options)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for renaming a DB table. - * @param string $table the table to be renamed. The name will be properly quoted by the method. - * @param string $newName the new table name. The name will be properly quoted by the method. - */ - public function renameTable($table, $newName) - { - echo " > rename table $table to $newName ..."; - $time = microtime(true); - $this->db->createCommand()->renameTable($table, $newName)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for dropping a DB table. - * @param string $table the table to be dropped. The name will be properly quoted by the method. - */ - public function dropTable($table) - { - echo " > drop table $table ..."; - $time = microtime(true); - $this->db->createCommand()->dropTable($table)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for truncating a DB table. - * @param string $table the table to be truncated. The name will be properly quoted by the method. - */ - public function truncateTable($table) - { - echo " > truncate table $table ..."; - $time = microtime(true); - $this->db->createCommand()->truncateTable($table)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for adding a new DB column. - * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. - * @param string $column the name of the new column. The name will be properly quoted by the method. - * @param string $type the column type. The [[QueryBuilder::getColumnType()]] method will be invoked to convert abstract column type (if any) - * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. - * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. - */ - public function addColumn($table, $column, $type) - { - echo " > add column $column $type to table $table ..."; - $time = microtime(true); - $this->db->createCommand()->addColumn($table, $column, $type)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for dropping a DB column. - * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. - * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. - */ - public function dropColumn($table, $column) - { - echo " > drop column $column from table $table ..."; - $time = microtime(true); - $this->db->createCommand()->dropColumn($table, $column)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for renaming a column. - * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. - * @param string $name the old name of the column. The name will be properly quoted by the method. - * @param string $newName the new name of the column. The name will be properly quoted by the method. - */ - public function renameColumn($table, $name, $newName) - { - echo " > rename column $name in table $table to $newName ..."; - $time = microtime(true); - $this->db->createCommand()->renameColumn($table, $name, $newName)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for changing the definition of a column. - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the new column type. The [[QueryBuilder::getColumnType()]] method will be invoked to convert abstract column type (if any) - * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. - * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. - */ - public function alterColumn($table, $column, $type) - { - echo " > alter column $column in table $table to $type ..."; - $time = microtime(true); - $this->db->createCommand()->alterColumn($table, $column, $type)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for creating a primary key. - * The method will properly quote the table and column names. - * @param string $name the name of the primary key constraint. - * @param string $table the table that the primary key constraint will be added to. - * @param string|array $columns comma separated string or array of columns that the primary key will consist of. - */ - public function addPrimaryKey($name, $table, $columns) - { - echo " > add primary key $name on $table (" . (is_array($columns) ? implode(',', $columns) : $columns).") ..."; - $time = microtime(true); - $this->db->createCommand()->addPrimaryKey($name, $table, $columns)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for dropping a primary key. - * @param string $name the name of the primary key constraint to be removed. - * @param string $table the table that the primary key constraint will be removed from. - */ - public function dropPrimaryKey($name, $table) - { - echo " > drop primary key $name ..."; - $time = microtime(true); - $this->db->createCommand()->dropPrimaryKey($name, $table)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds a SQL statement for adding a foreign key constraint to an existing table. - * The method will properly quote the table and column names. - * @param string $name the name of the foreign key constraint. - * @param string $table the table that the foreign key constraint will be added to. - * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas or use an array. - * @param string $refTable the table that the foreign key references to. - * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas or use an array. - * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - */ - public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) - { - echo " > add foreign key $name: $table (" . implode(',', (array)$columns) . ") references $refTable (" . implode(',', (array)$refColumns) . ") ..."; - $time = microtime(true); - $this->db->createCommand()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds a SQL statement for dropping a foreign key constraint. - * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. - */ - public function dropForeignKey($name, $table) - { - echo " > drop foreign key $name from table $table ..."; - $time = microtime(true); - $this->db->createCommand()->dropForeignKey($name, $table)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for creating a new index. - * @param string $name the name of the index. The name will be properly quoted by the method. - * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. - * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns, please separate them - * by commas or use an array. The column names will be properly quoted by the method. - * @param boolean $unique whether to add UNIQUE constraint on the created index. - */ - public function createIndex($name, $table, $columns, $unique = false) - { - echo " > create" . ($unique ? ' unique' : '') . " index $name on $table (" . implode(',', (array)$columns) . ") ..."; - $time = microtime(true); - $this->db->createCommand()->createIndex($name, $table, $columns, $unique)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } - - /** - * Builds and executes a SQL statement for dropping an index. - * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. - */ - public function dropIndex($name, $table) - { - echo " > drop index $name ..."; - $time = microtime(true); - $this->db->createCommand()->dropIndex($name, $table)->execute(); - echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; - } + /** + * @var Connection the database connection that this migration should work with. + * If not set, it will be initialized as the 'db' application component. + */ + public $db; + + /** + * Initializes the migration. + * This method will set [[db]] to be the 'db' application component, if it is null. + */ + public function init() + { + parent::init(); + if ($this->db === null) { + $this->db = \Yii::$app->getComponent('db'); + } + } + + /** + * This method contains the logic to be executed when applying this migration. + * Child classes may overwrite this method to provide actual migration logic. + * @return boolean return a false value to indicate the migration fails + * and should not proceed further. All other return values mean the migration succeeds. + */ + public function up() + { + $transaction = $this->db->beginTransaction(); + try { + if ($this->safeUp() === false) { + $transaction->rollBack(); + + return false; + } + $transaction->commit(); + } catch (\Exception $e) { + echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; + echo $e->getTraceAsString() . "\n"; + $transaction->rollBack(); + + return false; + } + + return null; + } + + /** + * This method contains the logic to be executed when removing this migration. + * The default implementation throws an exception indicating the migration cannot be removed. + * Child classes may override this method if the corresponding migrations can be removed. + * @return boolean return a false value to indicate the migration fails + * and should not proceed further. All other return values mean the migration succeeds. + */ + public function down() + { + $transaction = $this->db->beginTransaction(); + try { + if ($this->safeDown() === false) { + $transaction->rollBack(); + + return false; + } + $transaction->commit(); + } catch (\Exception $e) { + echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; + echo $e->getTraceAsString() . "\n"; + $transaction->rollBack(); + + return false; + } + + return null; + } + + /** + * This method contains the logic to be executed when applying this migration. + * This method differs from [[up()]] in that the DB logic implemented here will + * be enclosed within a DB transaction. + * Child classes may implement this method instead of [[up()]] if the DB logic + * needs to be within a transaction. + * @return boolean return a false value to indicate the migration fails + * and should not proceed further. All other return values mean the migration succeeds. + */ + public function safeUp() + { + } + + /** + * This method contains the logic to be executed when removing this migration. + * This method differs from [[down()]] in that the DB logic implemented here will + * be enclosed within a DB transaction. + * Child classes may implement this method instead of [[up()]] if the DB logic + * needs to be within a transaction. + * @return boolean return a false value to indicate the migration fails + * and should not proceed further. All other return values mean the migration succeeds. + */ + public function safeDown() + { + } + + /** + * Executes a SQL statement. + * This method executes the specified SQL statement using [[db]]. + * @param string $sql the SQL statement to be executed + * @param array $params input parameters (name => value) for the SQL execution. + * See [[Command::execute()]] for more details. + */ + public function execute($sql, $params = []) + { + echo " > execute SQL: $sql ..."; + $time = microtime(true); + $this->db->createCommand($sql)->bindValues($params)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes an INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name => value) to be inserted into the table. + */ + public function insert($table, $columns) + { + echo " > insert into $table ..."; + $time = microtime(true); + $this->db->createCommand()->insert($table, $columns)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes an batch INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names. + * @param array $rows the rows to be batch inserted into the table + */ + public function batchInsert($table, $columns, $rows) + { + echo " > insert into $table ..."; + $time = microtime(true); + $this->db->createCommand()->batchInsert($table, $columns, $rows)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes an UPDATE SQL statement. + * The method will properly escape the column names and bind the values to be updated. + * @param string $table the table to be updated. + * @param array $columns the column data (name => value) to be updated. + * @param array|string $condition the conditions that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify conditions. + * @param array $params the parameters to be bound to the query. + */ + public function update($table, $columns, $condition = '', $params = []) + { + echo " > update $table ..."; + $time = microtime(true); + $this->db->createCommand()->update($table, $columns, $condition, $params)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Creates and executes a DELETE SQL statement. + * @param string $table the table where the data will be deleted from. + * @param array|string $condition the conditions that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify conditions. + * @param array $params the parameters to be bound to the query. + */ + public function delete($table, $condition = '', $params = []) + { + echo " > delete from $table ..."; + $time = microtime(true); + $this->db->createCommand()->delete($table, $condition, $params)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * + * The [[QueryBuilder::getColumnType()]] method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * put into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name => definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + */ + public function createTable($table, $columns, $options = null) + { + echo " > create table $table ..."; + $time = microtime(true); + $this->db->createCommand()->createTable($table, $columns, $options)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + */ + public function renameTable($table, $newName) + { + echo " > rename table $table to $newName ..."; + $time = microtime(true); + $this->db->createCommand()->renameTable($table, $newName)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + */ + public function dropTable($table) + { + echo " > drop table $table ..."; + $time = microtime(true); + $this->db->createCommand()->dropTable($table)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + */ + public function truncateTable($table) + { + echo " > truncate table $table ..."; + $time = microtime(true); + $this->db->createCommand()->truncateTable($table)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The [[QueryBuilder::getColumnType()]] method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + */ + public function addColumn($table, $column, $type) + { + echo " > add column $column $type to table $table ..."; + $time = microtime(true); + $this->db->createCommand()->addColumn($table, $column, $type)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + */ + public function dropColumn($table, $column) + { + echo " > drop column $column from table $table ..."; + $time = microtime(true); + $this->db->createCommand()->dropColumn($table, $column)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + */ + public function renameColumn($table, $name, $newName) + { + echo " > rename column $name in table $table to $newName ..."; + $time = microtime(true); + $this->db->createCommand()->renameColumn($table, $name, $newName)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The [[QueryBuilder::getColumnType()]] method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + */ + public function alterColumn($table, $column, $type) + { + echo " > alter column $column in table $table to $type ..."; + $time = microtime(true); + $this->db->createCommand()->alterColumn($table, $column, $type)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a primary key. + * The method will properly quote the table and column names. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + */ + public function addPrimaryKey($name, $table, $columns) + { + echo " > add primary key $name on $table (" . (is_array($columns) ? implode(',', $columns) : $columns).") ..."; + $time = microtime(true); + $this->db->createCommand()->addPrimaryKey($name, $table, $columns)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a primary key. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + */ + public function dropPrimaryKey($name, $table) + { + echo " > drop primary key $name ..."; + $time = microtime(true); + $this->db->createCommand()->dropPrimaryKey($name, $table)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas or use an array. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas or use an array. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + echo " > add foreign key $name: $table (" . implode(',', (array) $columns) . ") references $refTable (" . implode(',', (array) $refColumns) . ") ..."; + $time = microtime(true); + $this->db->createCommand()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + */ + public function dropForeignKey($name, $table) + { + echo " > drop foreign key $name from table $table ..."; + $time = microtime(true); + $this->db->createCommand()->dropForeignKey($name, $table)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas or use an array. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + */ + public function createIndex($name, $table, $columns, $unique = false) + { + echo " > create" . ($unique ? ' unique' : '') . " index $name on $table (" . implode(',', (array) $columns) . ") ..."; + $time = microtime(true); + $this->db->createCommand()->createIndex($name, $table, $columns, $unique)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + */ + public function dropIndex($name, $table) + { + echo " > drop index $name ..."; + $time = microtime(true); + $this->db->createCommand()->dropIndex($name, $table)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } } diff --git a/framework/db/Query.php b/framework/db/Query.php index 18f8c02b3c9..1971e783cb4 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -41,763 +41,784 @@ */ class Query extends Component implements QueryInterface { - use QueryTrait; - - /** - * @var array the columns being selected. For example, `['id', 'name']`. - * This is used to construct the SELECT clause in a SQL statement. If not set, it means selecting all columns. - * @see select() - */ - public $select; - /** - * @var string additional option that should be appended to the 'SELECT' keyword. For example, - * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - */ - public $selectOption; - /** - * @var boolean whether to select distinct rows of data only. If this is set true, - * the SELECT clause would be changed to SELECT DISTINCT. - */ - public $distinct; - /** - * @var array the table(s) to be selected from. For example, `['tbl_user', 'tbl_post']`. - * This is used to construct the FROM clause in a SQL statement. - * @see from() - */ - public $from; - /** - * @var array how to group the query results. For example, `['company', 'department']`. - * This is used to construct the GROUP BY clause in a SQL statement. - */ - public $groupBy; - /** - * @var array how to join with other tables. Each array element represents the specification - * of one join which has the following structure: - * - * ~~~ - * [$joinType, $tableName, $joinCondition] - * ~~~ - * - * For example, - * - * ~~~ - * [ - * ['INNER JOIN', 'tbl_user', 'tbl_user.id = author_id'], - * ['LEFT JOIN', 'tbl_team', 'tbl_team.id = team_id'], - * ] - * ~~~ - */ - public $join; - /** - * @var string|array the condition to be applied in the GROUP BY clause. - * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. - */ - public $having; - /** - * @var array this is used to construct the UNION clause(s) in a SQL statement. - * Each array element is an array of the following structure: - * - * - `query`: either a string or a [[Query]] object representing a query - * - `all`: boolean, whether it should be `UNION ALL` or `UNION` - */ - public $union; - /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. - */ - public $params = []; - - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($db === null) { - $db = Yii::$app->getDb(); - } - list ($sql, $params) = $db->getQueryBuilder()->build($this); - return $db->createCommand($sql, $params); - } - - /** - * Starts a batch query. - * - * A batch query supports fetching data in batches, which can keep the memory usage under a limit. - * This method will return a [[BatchQueryResult]] object which implements the `Iterator` interface - * and can be traversed to retrieve the data in batches. - * - * For example, - * - * ```php - * $query = (new Query)->from('tbl_user'); - * foreach ($query->batch() as $rows) { - * // $rows is an array of 10 or fewer rows from tbl_user - * } - * ``` - * - * @param integer $batchSize the number of records to be fetched in each batch. - * @param Connection $db the database connection. If not set, the "db" application component will be used. - * @return BatchQueryResult the batch query result. It implements the `Iterator` interface - * and can be traversed to retrieve the data in batches. - */ - public function batch($batchSize = 100, $db = null) - { - return Yii::createObject([ - 'class' => BatchQueryResult::className(), - 'query' => $this, - 'batchSize' => $batchSize, - 'db' => $db, - 'each' => false, - ]); - } - - /** - * Starts a batch query and retrieves data row by row. - * This method is similar to [[batch()]] except that in each iteration of the result, - * only one row of data is returned. For example, - * - * ```php - * $query = (new Query)->from('tbl_user'); - * foreach ($query->each() as $row) { - * } - * ``` - * - * @param integer $batchSize the number of records to be fetched in each batch. - * @param Connection $db the database connection. If not set, the "db" application component will be used. - * @return BatchQueryResult the batch query result. It implements the `Iterator` interface - * and can be traversed to retrieve the data in batches. - */ - public function each($batchSize = 100, $db = null) - { - return Yii::createObject([ - 'class' => BatchQueryResult::className(), - 'query' => $this, - 'batchSize' => $batchSize, - 'db' => $db, - 'each' => true, - ]); - } - - /** - * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $rows = $this->createCommand($db)->queryAll(); - return $this->prepareResult($rows); - } - - /** - * Converts the raw query results into the format as specified by this query. - * This method is internally used to convert the data fetched from database - * into the format as required by this query. - * @param array $rows the raw query result from database - * @return array the converted query result - */ - public function prepareResult($rows) - { - if ($this->indexBy === null) { - return $rows; - } - $result = []; - foreach ($rows as $row) { - if (is_string($this->indexBy)) { - $key = $row[$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - $result[$key] = $row; - } - return $result; - } - - /** - * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - public function one($db = null) - { - return $this->createCommand($db)->queryOne(); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the first column in the first row of the query results. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if the query result is empty. - */ - public function scalar($db = null) - { - return $this->createCommand($db)->queryScalar(); - } - - /** - * Executes the query and returns the first column of the result. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($db = null) - { - return $this->createCommand($db)->queryColumn(); - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. Defaults to '*'. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given (or null), the `db` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null) - { - return $this->queryScalar("COUNT($q)", $db); - } - - /** - * Returns the sum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return integer the sum of the specified column values - */ - public function sum($q, $db = null) - { - return $this->queryScalar("SUM($q)", $db); - } - - /** - * Returns the average of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return integer the average of the specified column values. - */ - public function average($q, $db = null) - { - return $this->queryScalar("AVG($q)", $db); - } - - /** - * Returns the minimum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return integer the minimum of the specified column values. - */ - public function min($q, $db = null) - { - return $this->queryScalar("MIN($q)", $db); - } - - /** - * Returns the maximum of the specified column values. - * @param string $q the column name or expression. - * Make sure you properly quote column names in the expression. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return integer the maximum of the specified column values. - */ - public function max($q, $db = null) - { - return $this->queryScalar("MAX($q)", $db); - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - $select = $this->select; - $this->select = [new Expression('1')]; - $command = $this->createCommand($db); - $this->select = $select; - return $command->queryScalar() !== false; - } - - /** - * Queries a scalar value by setting [[select]] first. - * Restores the value of select to make this query reusable. - * @param string|Expression $selectExpression - * @param Connection|null $db - * @return bool|string - */ - private function queryScalar($selectExpression, $db) - { - $select = $this->select; - $limit = $this->limit; - $offset = $this->offset; - - $this->select = [$selectExpression]; - $this->limit = null; - $this->offset = null; - $command = $this->createCommand($db); - - $this->select = $select; - $this->limit = $limit; - $this->offset = $offset; - - if (empty($this->groupBy) && !$this->distinct) { - return $command->queryScalar(); - } else { - return (new Query)->select([$selectExpression]) - ->from(['c' => $this]) - ->createCommand($db) - ->queryScalar(); - } - } - - /** - * Sets the SELECT part of the query. - * @param string|array $columns the columns to be selected. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). - * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * - * Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should - * use an array to specify the columns. Otherwise, the expression may be incorrectly split into several parts. - * - * When the columns are specified as an array, you may also use array keys as the column aliases (if a column - * does not need alias, do not use a string key). - * - * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, - * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - * @return static the query object itself - */ - public function select($columns, $option = null) - { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->select = $columns; - $this->selectOption = $option; - return $this; - } - - /** - * Sets the value indicating whether to SELECT DISTINCT or not. - * @param boolean $value whether to SELECT DISTINCT or not. - * @return static the query object itself - */ - public function distinct($value = true) - { - $this->distinct = $value; - return $this; - } - - /** - * Sets the FROM part of the query. - * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. `'tbl_user'`) - * or an array (e.g. `['tbl_user', 'tbl_profile']`) specifying one or several table names. - * Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`). - * The method will automatically quote the table names unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * - * When the tables are specified as an array, you may also use the array keys as the table aliases - * (if a table does not need alias, do not use a string key). - * - * Use a Query object to represent a sub-query. In this case, the corresponding array key will be used - * as the alias for the sub-query. - * - * @return static the query object itself - */ - public function from($tables) - { - if (!is_array($tables)) { - $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); - } - $this->from = $tables; - return $this; - } - - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter, and optionally a $params parameter - * specifying the values to be bound to the query. - * - * The $condition parameter should be either a string (e.g. 'id=1') or an array. - * If the latter, it must be in one of the following two formats: - * - * - hash format: `['column1' => value1, 'column2' => value2, ...]` - * - operator format: `[operator, operand1, operand2, ...]` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. - * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. - * - `['status' => null] generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape special characters in the values. - * Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply - * a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. - * - * - `exists`: requires one operand which must be an instance of [[Query]] representing the sub-query. - * It will build a `EXISTS (sub-query)` expression. - * - * - `not exists`: similar to the `exists` operator and builds a `NOT EXISTS (sub-query)` expression. - * - * @param string|array $condition the conditions that should be put in the WHERE part. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition, $params = []) - { - $this->where = $condition; - $this->addParams($params); - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition, $params = []) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['and', $this->where, $condition]; - } - $this->addParams($params); - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition, $params = []) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['or', $this->where, $condition]; - } - $this->addParams($params); - return $this; - } - - /** - * Appends a JOIN part to the query. - * The first parameter specifies what type of join it is. - * @param string $type the type of join, such as INNER JOIN, LEFT JOIN. - * @param string|array $table the table to be joined. - * - * Use string to represent the name of the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * - * Use array to represent joining with a sub-query. The array must contain only one element. - * The value must be a Query object representing the sub-query while the corresponding key - * represents the alias for the sub-query. - * - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return Query the query object itself - */ - public function join($type, $table, $on = '', $params = []) - { - $this->join[] = [$type, $table, $on]; - return $this->addParams($params); - } - - /** - * Appends an INNER JOIN part to the query. - * @param string|array $table the table to be joined. - * - * Use string to represent the name of the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * - * Use array to represent joining with a sub-query. The array must contain only one element. - * The value must be a Query object representing the sub-query while the corresponding key - * represents the alias for the sub-query. - * - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return Query the query object itself - */ - public function innerJoin($table, $on = '', $params = []) - { - $this->join[] = ['INNER JOIN', $table, $on]; - return $this->addParams($params); - } - - /** - * Appends a LEFT OUTER JOIN part to the query. - * @param string|array $table the table to be joined. - * - * Use string to represent the name of the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * - * Use array to represent joining with a sub-query. The array must contain only one element. - * The value must be a Query object representing the sub-query while the corresponding key - * represents the alias for the sub-query. - * - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query - * @return Query the query object itself - */ - public function leftJoin($table, $on = '', $params = []) - { - $this->join[] = ['LEFT JOIN', $table, $on]; - return $this->addParams($params); - } - - /** - * Appends a RIGHT OUTER JOIN part to the query. - * @param string|array $table the table to be joined. - * - * Use string to represent the name of the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * - * Use array to represent joining with a sub-query. The array must contain only one element. - * The value must be a Query object representing the sub-query while the corresponding key - * represents the alias for the sub-query. - * - * @param string|array $on the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query - * @return Query the query object itself - */ - public function rightJoin($table, $on = '', $params = []) - { - $this->join[] = ['RIGHT JOIN', $table, $on]; - return $this->addParams($params); - } - - /** - * Sets the GROUP BY part of the query. - * @param string|array $columns the columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see addGroupBy() - */ - public function groupBy($columns) - { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->groupBy = $columns; - return $this; - } - - /** - * Adds additional group-by columns to the existing ones. - * @param string|array $columns additional columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see groupBy() - */ - public function addGroupBy($columns) - { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - if ($this->groupBy === null) { - $this->groupBy = $columns; - } else { - $this->groupBy = array_merge($this->groupBy, $columns); - } - return $this; - } - - /** - * Sets the HAVING part of the query. - * @param string|array $condition the conditions to be put after HAVING. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see andHaving() - * @see orHaving() - */ - public function having($condition, $params = []) - { - $this->having = $condition; - $this->addParams($params); - return $this; - } - - /** - * Adds an additional HAVING condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new HAVING condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see having() - * @see orHaving() - */ - public function andHaving($condition, $params = []) - { - if ($this->having === null) { - $this->having = $condition; - } else { - $this->having = ['and', $this->having, $condition]; - } - $this->addParams($params); - return $this; - } - - /** - * Adds an additional HAVING condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new HAVING condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name => value) to be bound to the query. - * @return static the query object itself - * @see having() - * @see andHaving() - */ - public function orHaving($condition, $params = []) - { - if ($this->having === null) { - $this->having = $condition; - } else { - $this->having = ['or', $this->having, $condition]; - } - $this->addParams($params); - return $this; - } - - /** - * Appends a SQL statement using UNION operator. - * @param string|Query $sql the SQL statement to be appended using UNION - * @param boolean $all TRUE if using UNION ALL and FALSE if using UNION - * @return static the query object itself - */ - public function union($sql, $all = false) - { - $this->union[] = [ 'query' => $sql, 'all' => $all ]; - return $this; - } - - /** - * Sets the parameters to be bound to the query. - * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. - * @return static the query object itself - * @see addParams() - */ - public function params($params) - { - $this->params = $params; - return $this; - } - - /** - * Adds additional parameters to be bound to the query. - * @param array $params list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. - * @return static the query object itself - * @see params() - */ - public function addParams($params) - { - if (!empty($params)) { - if (empty($this->params)) { - $this->params = $params; - } else { - foreach ($params as $name => $value) { - if (is_integer($name)) { - $this->params[] = $value; - } else { - $this->params[$name] = $value; - } - } - } - } - return $this; - } + use QueryTrait; + + /** + * @var array the columns being selected. For example, `['id', 'name']`. + * This is used to construct the SELECT clause in a SQL statement. If not set, it means selecting all columns. + * @see select() + */ + public $select; + /** + * @var string additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. + */ + public $selectOption; + /** + * @var boolean whether to select distinct rows of data only. If this is set true, + * the SELECT clause would be changed to SELECT DISTINCT. + */ + public $distinct; + /** + * @var array the table(s) to be selected from. For example, `['tbl_user', 'tbl_post']`. + * This is used to construct the FROM clause in a SQL statement. + * @see from() + */ + public $from; + /** + * @var array how to group the query results. For example, `['company', 'department']`. + * This is used to construct the GROUP BY clause in a SQL statement. + */ + public $groupBy; + /** + * @var array how to join with other tables. Each array element represents the specification + * of one join which has the following structure: + * + * ~~~ + * [$joinType, $tableName, $joinCondition] + * ~~~ + * + * For example, + * + * ~~~ + * [ + * ['INNER JOIN', 'tbl_user', 'tbl_user.id = author_id'], + * ['LEFT JOIN', 'tbl_team', 'tbl_team.id = team_id'], + * ] + * ~~~ + */ + public $join; + /** + * @var string|array the condition to be applied in the GROUP BY clause. + * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. + */ + public $having; + /** + * @var array this is used to construct the UNION clause(s) in a SQL statement. + * Each array element is an array of the following structure: + * + * - `query`: either a string or a [[Query]] object representing a query + * - `all`: boolean, whether it should be `UNION ALL` or `UNION` + */ + public $union; + /** + * @var array list of query parameter values indexed by parameter placeholders. + * For example, `[':name' => 'Dan', ':age' => 31]`. + */ + public $params = []; + + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + if ($db === null) { + $db = Yii::$app->getDb(); + } + list ($sql, $params) = $db->getQueryBuilder()->build($this); + + return $db->createCommand($sql, $params); + } + + /** + * Starts a batch query. + * + * A batch query supports fetching data in batches, which can keep the memory usage under a limit. + * This method will return a [[BatchQueryResult]] object which implements the `Iterator` interface + * and can be traversed to retrieve the data in batches. + * + * For example, + * + * ```php + * $query = (new Query)->from('tbl_user'); + * foreach ($query->batch() as $rows) { + * // $rows is an array of 10 or fewer rows from tbl_user + * } + * ``` + * + * @param integer $batchSize the number of records to be fetched in each batch. + * @param Connection $db the database connection. If not set, the "db" application component will be used. + * @return BatchQueryResult the batch query result. It implements the `Iterator` interface + * and can be traversed to retrieve the data in batches. + */ + public function batch($batchSize = 100, $db = null) + { + return Yii::createObject([ + 'class' => BatchQueryResult::className(), + 'query' => $this, + 'batchSize' => $batchSize, + 'db' => $db, + 'each' => false, + ]); + } + + /** + * Starts a batch query and retrieves data row by row. + * This method is similar to [[batch()]] except that in each iteration of the result, + * only one row of data is returned. For example, + * + * ```php + * $query = (new Query)->from('tbl_user'); + * foreach ($query->each() as $row) { + * } + * ``` + * + * @param integer $batchSize the number of records to be fetched in each batch. + * @param Connection $db the database connection. If not set, the "db" application component will be used. + * @return BatchQueryResult the batch query result. It implements the `Iterator` interface + * and can be traversed to retrieve the data in batches. + */ + public function each($batchSize = 100, $db = null) + { + return Yii::createObject([ + 'class' => BatchQueryResult::className(), + 'query' => $this, + 'batchSize' => $batchSize, + 'db' => $db, + 'each' => true, + ]); + } + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $rows = $this->createCommand($db)->queryAll(); + + return $this->prepareResult($rows); + } + + /** + * Converts the raw query results into the format as specified by this query. + * This method is internally used to convert the data fetched from database + * into the format as required by this query. + * @param array $rows the raw query result from database + * @return array the converted query result + */ + public function prepareResult($rows) + { + if ($this->indexBy === null) { + return $rows; + } + $result = []; + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $result[$key] = $row; + } + + return $result; + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null) + { + return $this->createCommand($db)->queryOne(); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the first column in the first row of the query results. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if the query result is empty. + */ + public function scalar($db = null) + { + return $this->createCommand($db)->queryScalar(); + } + + /** + * Executes the query and returns the first column of the result. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($db = null) + { + return $this->createCommand($db)->queryColumn(); + } + + /** + * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given (or null), the `db` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null) + { + return $this->queryScalar("COUNT($q)", $db); + } + + /** + * Returns the sum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the sum of the specified column values + */ + public function sum($q, $db = null) + { + return $this->queryScalar("SUM($q)", $db); + } + + /** + * Returns the average of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($q, $db = null) + { + return $this->queryScalar("AVG($q)", $db); + } + + /** + * Returns the minimum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($q, $db = null) + { + return $this->queryScalar("MIN($q)", $db); + } + + /** + * Returns the maximum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($q, $db = null) + { + return $this->queryScalar("MAX($q)", $db); + } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + $select = $this->select; + $this->select = [new Expression('1')]; + $command = $this->createCommand($db); + $this->select = $select; + + return $command->queryScalar() !== false; + } + + /** + * Queries a scalar value by setting [[select]] first. + * Restores the value of select to make this query reusable. + * @param string|Expression $selectExpression + * @param Connection|null $db + * @return bool|string + */ + private function queryScalar($selectExpression, $db) + { + $select = $this->select; + $limit = $this->limit; + $offset = $this->offset; + + $this->select = [$selectExpression]; + $this->limit = null; + $this->offset = null; + $command = $this->createCommand($db); + + $this->select = $select; + $this->limit = $limit; + $this->offset = $offset; + + if (empty($this->groupBy) && !$this->distinct) { + return $command->queryScalar(); + } else { + return (new Query)->select([$selectExpression]) + ->from(['c' => $this]) + ->createCommand($db) + ->queryScalar(); + } + } + + /** + * Sets the SELECT part of the query. + * @param string|array $columns the columns to be selected. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). + * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * + * Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should + * use an array to specify the columns. Otherwise, the expression may be incorrectly split into several parts. + * + * When the columns are specified as an array, you may also use array keys as the column aliases (if a column + * does not need alias, do not use a string key). + * + * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. + * @return static the query object itself + */ + public function select($columns, $option = null) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->select = $columns; + $this->selectOption = $option; + + return $this; + } + + /** + * Sets the value indicating whether to SELECT DISTINCT or not. + * @param boolean $value whether to SELECT DISTINCT or not. + * @return static the query object itself + */ + public function distinct($value = true) + { + $this->distinct = $value; + + return $this; + } + + /** + * Sets the FROM part of the query. + * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. `'tbl_user'`) + * or an array (e.g. `['tbl_user', 'tbl_profile']`) specifying one or several table names. + * Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`). + * The method will automatically quote the table names unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * + * When the tables are specified as an array, you may also use the array keys as the table aliases + * (if a table does not need alias, do not use a string key). + * + * Use a Query object to represent a sub-query. In this case, the corresponding array key will be used + * as the alias for the sub-query. + * + * @return static the query object itself + */ + public function from($tables) + { + if (!is_array($tables)) { + $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); + } + $this->from = $tables; + + return $this; + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $condition parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be in one of the following two formats: + * + * - hash format: `['column1' => value1, 'column2' => value2, ...]` + * - operator format: `[operator, operand1, operand2, ...]` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. + * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `['status' => null] generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape special characters in the values. + * Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply + * a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * - `exists`: requires one operand which must be an instance of [[Query]] representing the sub-query. + * It will build a `EXISTS (sub-query)` expression. + * + * - `not exists`: similar to the `exists` operator and builds a `NOT EXISTS (sub-query)` expression. + * + * @param string|array $condition the conditions that should be put in the WHERE part. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition, $params = []) + { + $this->where = $condition; + $this->addParams($params); + + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition, $params = []) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['and', $this->where, $condition]; + } + $this->addParams($params); + + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition, $params = []) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['or', $this->where, $condition]; + } + $this->addParams($params); + + return $this; + } + + /** + * Appends a JOIN part to the query. + * The first parameter specifies what type of join it is. + * @param string $type the type of join, such as INNER JOIN, LEFT JOIN. + * @param string|array $table the table to be joined. + * + * Use string to represent the name of the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * + * Use array to represent joining with a sub-query. The array must contain only one element. + * The value must be a Query object representing the sub-query while the corresponding key + * represents the alias for the sub-query. + * + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return Query the query object itself + */ + public function join($type, $table, $on = '', $params = []) + { + $this->join[] = [$type, $table, $on]; + + return $this->addParams($params); + } + + /** + * Appends an INNER JOIN part to the query. + * @param string|array $table the table to be joined. + * + * Use string to represent the name of the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * + * Use array to represent joining with a sub-query. The array must contain only one element. + * The value must be a Query object representing the sub-query while the corresponding key + * represents the alias for the sub-query. + * + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return Query the query object itself + */ + public function innerJoin($table, $on = '', $params = []) + { + $this->join[] = ['INNER JOIN', $table, $on]; + + return $this->addParams($params); + } + + /** + * Appends a LEFT OUTER JOIN part to the query. + * @param string|array $table the table to be joined. + * + * Use string to represent the name of the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * + * Use array to represent joining with a sub-query. The array must contain only one element. + * The value must be a Query object representing the sub-query while the corresponding key + * represents the alias for the sub-query. + * + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query + * @return Query the query object itself + */ + public function leftJoin($table, $on = '', $params = []) + { + $this->join[] = ['LEFT JOIN', $table, $on]; + + return $this->addParams($params); + } + + /** + * Appends a RIGHT OUTER JOIN part to the query. + * @param string|array $table the table to be joined. + * + * Use string to represent the name of the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * + * Use array to represent joining with a sub-query. The array must contain only one element. + * The value must be a Query object representing the sub-query while the corresponding key + * represents the alias for the sub-query. + * + * @param string|array $on the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query + * @return Query the query object itself + */ + public function rightJoin($table, $on = '', $params = []) + { + $this->join[] = ['RIGHT JOIN', $table, $on]; + + return $this->addParams($params); + } + + /** + * Sets the GROUP BY part of the query. + * @param string|array $columns the columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addGroupBy() + */ + public function groupBy($columns) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->groupBy = $columns; + + return $this; + } + + /** + * Adds additional group-by columns to the existing ones. + * @param string|array $columns additional columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see groupBy() + */ + public function addGroupBy($columns) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + if ($this->groupBy === null) { + $this->groupBy = $columns; + } else { + $this->groupBy = array_merge($this->groupBy, $columns); + } + + return $this; + } + + /** + * Sets the HAVING part of the query. + * @param string|array $condition the conditions to be put after HAVING. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see andHaving() + * @see orHaving() + */ + public function having($condition, $params = []) + { + $this->having = $condition; + $this->addParams($params); + + return $this; + } + + /** + * Adds an additional HAVING condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new HAVING condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see having() + * @see orHaving() + */ + public function andHaving($condition, $params = []) + { + if ($this->having === null) { + $this->having = $condition; + } else { + $this->having = ['and', $this->having, $condition]; + } + $this->addParams($params); + + return $this; + } + + /** + * Adds an additional HAVING condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new HAVING condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return static the query object itself + * @see having() + * @see andHaving() + */ + public function orHaving($condition, $params = []) + { + if ($this->having === null) { + $this->having = $condition; + } else { + $this->having = ['or', $this->having, $condition]; + } + $this->addParams($params); + + return $this; + } + + /** + * Appends a SQL statement using UNION operator. + * @param string|Query $sql the SQL statement to be appended using UNION + * @param boolean $all TRUE if using UNION ALL and FALSE if using UNION + * @return static the query object itself + */ + public function union($sql, $all = false) + { + $this->union[] = [ 'query' => $sql, 'all' => $all ]; + + return $this; + } + + /** + * Sets the parameters to be bound to the query. + * @param array $params list of query parameter values indexed by parameter placeholders. + * For example, `[':name' => 'Dan', ':age' => 31]`. + * @return static the query object itself + * @see addParams() + */ + public function params($params) + { + $this->params = $params; + + return $this; + } + + /** + * Adds additional parameters to be bound to the query. + * @param array $params list of query parameter values indexed by parameter placeholders. + * For example, `[':name' => 'Dan', ':age' => 31]`. + * @return static the query object itself + * @see params() + */ + public function addParams($params) + { + if (!empty($params)) { + if (empty($this->params)) { + $this->params = $params; + } else { + foreach ($params as $name => $value) { + if (is_integer($name)) { + $this->params[] = $value; + } else { + $this->params[$name] = $value; + } + } + } + } + + return $this; + } } diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 124848b7573..3014418d012 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -21,1126 +21,1142 @@ */ class QueryBuilder extends \yii\base\Object { - /** - * The prefix for automatically generated query binding parameters. - */ - const PARAM_PREFIX = ':qp'; - - /** - * @var Connection the database connection. - */ - public $db; - /** - * @var string the separator between different fragments of a SQL statement. - * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement. - */ - public $separator = " "; - /** - * @var array the abstract column types mapped to physical column types. - * This is mainly used to support creating/modifying tables using DB-independent data type specifications. - * Child classes should override this property to declare supported type mappings. - */ - public $typeMap = []; - - /** - * Constructor. - * @param Connection $connection the database connection. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($connection, $config = []) - { - $this->db = $connection; - parent::__construct($config); - } - - /** - * Generates a SELECT SQL statement from a [[Query]] object. - * @param Query $query the [[Query]] object from which the SQL statement will be generated. - * @param array $params the parameters to be bound to the generated SQL statement. These parameters will - * be included in the result with the additional parameters generated during the query building process. - * @return array the generated SQL statement (the first array element) and the corresponding - * parameters to be bound to the SQL statement (the second array element). The parameters returned - * include those provided in `$params`. - */ - public function build($query, $params = []) - { - $params = empty($params) ? $query->params : array_merge($params, $query->params); - - $select = $query->select; - $from = $query->from; - if ($from === null && $query instanceof ActiveQuery) { - /** @var ActiveRecord $modelClass */ - $modelClass = $query->modelClass; - $tableName = $modelClass::tableName(); - $from = [$tableName]; - if ($select === null && !empty($query->join)) { - $select = ["$tableName.*"]; - } - } - - $clauses = [ - $this->buildSelect($select, $params, $query->distinct, $query->selectOption), - $this->buildFrom($from, $params), - $this->buildJoin($query->join, $params), - $this->buildWhere($query->where, $params), - $this->buildGroupBy($query->groupBy), - $this->buildHaving($query->having, $params), - $this->buildOrderBy($query->orderBy), - $this->buildLimit($query->limit, $query->offset), - $this->buildUnion($query->union, $params), - ]; - return [implode($this->separator, array_filter($clauses)), $params]; - } - - /** - * Creates an INSERT SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->insert('tbl_user', [ - * 'name' => 'Sam', - * 'age' => 30, - * ], $params); - * ~~~ - * - * The method will properly escape the table and column names. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name => value) to be inserted into the table. - * @param array $params the binding parameters that will be generated by this method. - * They should be bound to the DB command later. - * @return string the INSERT SQL - */ - public function insert($table, $columns, &$params) - { - if (($tableSchema = $this->db->getTableSchema($table)) !== null) { - $columnSchemas = $tableSchema->columns; - } else { - $columnSchemas = []; - } - $names = []; - $placeholders = []; - foreach ($columns as $name => $value) { - $names[] = $this->db->quoteColumnName($name); - if ($value instanceof Expression) { - $placeholders[] = $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $placeholders[] = $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; - } - } - - return 'INSERT INTO ' . $this->db->quoteTableName($table) - . ' (' . implode(', ', $names) . ') VALUES (' - . implode(', ', $placeholders) . ')'; - } - - /** - * Generates a batch INSERT SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->batchInsert('tbl_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ]); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the table - * @return string the batch INSERT SQL statement - */ - public function batchInsert($table, $columns, $rows) - { - if (($tableSchema = $this->db->getTableSchema($table)) !== null) { - $columnSchemas = $tableSchema->columns; - } else { - $columnSchemas = []; - } - - foreach ($columns as $i => $name) { - $columns[$i] = $this->db->quoteColumnName($name); - } - - $values = []; - foreach ($rows as $row) { - $vs = []; - foreach ($row as $i => $value) { - if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { - $value = $columnSchemas[$columns[$i]]->typecast($value); - } - $vs[] = is_string($value) ? $this->db->quoteValue($value) : ($value === null ? 'NULL' : $value); - } - $values[] = '(' . implode(', ', $vs) . ')'; - } - - return 'INSERT INTO ' . $this->db->quoteTableName($table) - . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); - } - - /** - * Creates an UPDATE SQL statement. - * For example, - * - * ~~~ - * $params = []; - * $sql = $queryBuilder->update('tbl_user', ['status' => 1], 'age > 30', $params); - * ~~~ - * - * The method will properly escape the table and column names. - * - * @param string $table the table to be updated. - * @param array $columns the column data (name => value) to be updated. - * @param array|string $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the binding parameters that will be modified by this method - * so that they can be bound to the DB command later. - * @return string the UPDATE SQL - */ - public function update($table, $columns, $condition, &$params) - { - if (($tableSchema = $this->db->getTableSchema($table)) !== null) { - $columnSchemas = $tableSchema->columns; - } else { - $columnSchemas = []; - } - - $lines = []; - foreach ($columns as $name => $value) { - if ($value instanceof Expression) { - $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; - } - } - - $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); - $where = $this->buildWhere($condition, $params); - return $where === '' ? $sql : $sql . ' ' . $where; - } - - /** - * Creates a DELETE SQL statement. - * For example, - * - * ~~~ - * $sql = $queryBuilder->delete('tbl_user', 'status = 0'); - * ~~~ - * - * The method will properly escape the table and column names. - * - * @param string $table the table where the data will be deleted from. - * @param array|string $condition the condition that will be put in the WHERE part. Please - * refer to [[Query::where()]] on how to specify condition. - * @param array $params the binding parameters that will be modified by this method - * so that they can be bound to the DB command later. - * @return string the DELETE SQL - */ - public function delete($table, $condition, &$params) - { - $sql = 'DELETE FROM ' . $this->db->quoteTableName($table); - $where = $this->buildWhere($condition, $params); - return $where === '' ? $sql : $sql . ' ' . $where; - } - - /** - * Builds a SQL statement for creating a new DB table. - * - * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), - * where name stands for a column name which will be properly quoted by the method, and definition - * stands for the column type which can contain an abstract DB type. - * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one. - * - * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly - * inserted into the generated SQL. - * - * For example, - * - * ~~~ - * $sql = $queryBuilder->createTable('tbl_user', [ - * 'id' => 'pk', - * 'name' => 'string', - * 'age' => 'integer', - * ]); - * ~~~ - * - * @param string $table the name of the table to be created. The name will be properly quoted by the method. - * @param array $columns the columns (name => definition) in the new table. - * @param string $options additional SQL fragment that will be appended to the generated SQL. - * @return string the SQL statement for creating a new DB table. - */ - public function createTable($table, $columns, $options = null) - { - $cols = []; - foreach ($columns as $name => $type) { - if (is_string($name)) { - $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type); - } else { - $cols[] = "\t" . $type; - } - } - $sql = "CREATE TABLE " . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)"; - return $options === null ? $sql : $sql . ' ' . $options; - } - - /** - * Builds a SQL statement for renaming a DB table. - * @param string $oldName the table to be renamed. The name will be properly quoted by the method. - * @param string $newName the new table name. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB table. - */ - public function renameTable($oldName, $newName) - { - return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName); - } - - /** - * Builds a SQL statement for dropping a DB table. - * @param string $table the table to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping a DB table. - */ - public function dropTable($table) - { - return "DROP TABLE " . $this->db->quoteTableName($table); - } - - /** - * Builds a SQL statement for adding a primary key constraint to an existing table. - * @param string $name the name of the primary key constraint. - * @param string $table the table that the primary key constraint will be added to. - * @param string|array $columns comma separated string or array of columns that the primary key will consist of. - * @return string the SQL statement for adding a primary key constraint to an existing table. - */ - public function addPrimaryKey($name, $table, $columns) - { - if (is_string($columns)) { - $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); - } - - foreach ($columns as $i => $col) { - $columns[$i] = $this->db->quoteColumnName($col); - } - - return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' - . $this->db->quoteColumnName($name) . ' PRIMARY KEY (' - . implode(', ', $columns). ' )'; - } - - /** - * Builds a SQL statement for removing a primary key constraint to an existing table. - * @param string $name the name of the primary key constraint to be removed. - * @param string $table the table that the primary key constraint will be removed from. - * @return string the SQL statement for removing a primary key constraint from an existing table. * - */ - public function dropPrimaryKey($name, $table) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) - . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); - } - - /** - * Builds a SQL statement for truncating a DB table. - * @param string $table the table to be truncated. The name will be properly quoted by the method. - * @return string the SQL statement for truncating a DB table. - */ - public function truncateTable($table) - { - return "TRUNCATE TABLE " . $this->db->quoteTableName($table); - } - - /** - * Builds a SQL statement for adding a new DB column. - * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. - * @param string $column the name of the new column. The name will be properly quoted by the method. - * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any) - * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. - * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. - * @return string the SQL statement for adding a new column. - */ - public function addColumn($table, $column, $type) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) - . ' ADD ' . $this->db->quoteColumnName($column) . ' ' - . $this->getColumnType($type); - } - - /** - * Builds a SQL statement for dropping a DB column. - * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. - * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping a DB column. - */ - public function dropColumn($table, $column) - { - return "ALTER TABLE " . $this->db->quoteTableName($table) - . " DROP COLUMN " . $this->db->quoteColumnName($column); - } - - /** - * Builds a SQL statement for renaming a column. - * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. - * @param string $oldName the old name of the column. The name will be properly quoted by the method. - * @param string $newName the new name of the column. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB column. - */ - public function renameColumn($table, $oldName, $newName) - { - return "ALTER TABLE " . $this->db->quoteTableName($table) - . " RENAME COLUMN " . $this->db->quoteColumnName($oldName) - . " TO " . $this->db->quoteColumnName($newName); - } - - /** - * Builds a SQL statement for changing the definition of a column. - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract - * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept - * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' - * will become 'varchar(255) not null'. - * @return string the SQL statement for changing the definition of a column. - */ - public function alterColumn($table, $column, $type) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE ' - . $this->db->quoteColumnName($column) . ' ' - . $this->db->quoteColumnName($column) . ' ' - . $this->getColumnType($type); - } - - /** - * Builds a SQL statement for adding a foreign key constraint to an existing table. - * The method will properly quote the table and column names. - * @param string $name the name of the foreign key constraint. - * @param string $table the table that the foreign key constraint will be added to. - * @param string|array $columns the name of the column to that the constraint will be added on. - * If there are multiple columns, separate them with commas or use an array to represent them. - * @param string $refTable the table that the foreign key references to. - * @param string|array $refColumns the name of the column that the foreign key references to. - * If there are multiple columns, separate them with commas or use an array to represent them. - * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @return string the SQL statement for adding a foreign key constraint to an existing table. - */ - public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) - { - $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) - . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name) - . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')' - . ' REFERENCES ' . $this->db->quoteTableName($refTable) - . ' (' . $this->buildColumns($refColumns) . ')'; - if ($delete !== null) { - $sql .= ' ON DELETE ' . $delete; - } - if ($update !== null) { - $sql .= ' ON UPDATE ' . $update; - } - return $sql; - } - - /** - * Builds a SQL statement for dropping a foreign key constraint. - * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping a foreign key constraint. - */ - public function dropForeignKey($name, $table) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) - . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); - } - - /** - * Builds a SQL statement for creating a new index. - * @param string $name the name of the index. The name will be properly quoted by the method. - * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. - * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns, - * separate them with commas or use an array to represent them. Each column name will be properly quoted - * by the method, unless a parenthesis is found in the name. - * @param boolean $unique whether to add UNIQUE constraint on the created index. - * @return string the SQL statement for creating a new index. - */ - public function createIndex($name, $table, $columns, $unique = false) - { - return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') - . $this->db->quoteTableName($name) . ' ON ' - . $this->db->quoteTableName($table) - . ' (' . $this->buildColumns($columns) . ')'; - } - - /** - * Builds a SQL statement for dropping an index. - * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping an index. - */ - public function dropIndex($name, $table) - { - return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table); - } - - /** - * Creates a SQL statement for resetting the sequence value of a table's primary key. - * The sequence will be reset such that the primary key of the next new row inserted - * will have the specified value or 1. - * @param string $table the name of the table whose primary key sequence will be reset - * @param array|string $value the value for the primary key of the next new row inserted. If this is not set, - * the next new row's primary key will have a value 1. - * @return string the SQL statement for resetting sequence - * @throws NotSupportedException if this is not supported by the underlying DBMS - */ - public function resetSequence($table, $value = null) - { - throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.'); - } - - /** - * Builds a SQL statement for enabling or disabling integrity check. - * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. - * @return string the SQL statement for checking integrity - * @throws NotSupportedException if this is not supported by the underlying DBMS - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.'); - } - - /** - * Converts an abstract column type into a physical column type. - * The conversion is done using the type map specified in [[typeMap]]. - * The following abstract column types are supported (using MySQL as an example to explain the corresponding - * physical types): - * - * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY" - * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY" - * - `string`: string type, will be converted into "varchar(255)" - * - `text`: a long string type, will be converted into "text" - * - `smallint`: a small integer type, will be converted into "smallint(6)" - * - `integer`: integer type, will be converted into "int(11)" - * - `bigint`: a big integer type, will be converted into "bigint(20)" - * - `boolean`: boolean type, will be converted into "tinyint(1)" - * - `float``: float number type, will be converted into "float" - * - `decimal`: decimal number type, will be converted into "decimal" - * - `datetime`: datetime type, will be converted into "datetime" - * - `timestamp`: timestamp type, will be converted into "timestamp" - * - `time`: time type, will be converted into "time" - * - `date`: date type, will be converted into "date" - * - `money`: money type, will be converted into "decimal(19,4)" - * - `binary`: binary data type, will be converted into "blob" - * - * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only - * the first part will be converted, and the rest of the parts will be appended to the converted result. - * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. - * - * For some of the abstract types you can also specify a length or precision constraint - * by prepending it in round brackets directly to the type. - * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. - * If the underlying DBMS does not support these kind of constraints for a type it will - * be ignored. - * - * If a type cannot be found in [[typeMap]], it will be returned without any change. - * @param string $type abstract column type - * @return string physical column type. - */ - public function getColumnType($type) - { - if (isset($this->typeMap[$type])) { - return $this->typeMap[$type]; - } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { - if (isset($this->typeMap[$matches[1]])) { - return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3]; - } - } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { - if (isset($this->typeMap[$matches[1]])) { - return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); - } - } - return $type; - } - - /** - * @param array $columns - * @param array $params the binding parameters to be populated - * @param boolean $distinct - * @param string $selectOption - * @return string the SELECT clause built from [[Query::$select]]. - */ - public function buildSelect($columns, &$params, $distinct = false, $selectOption = null) - { - $select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; - if ($selectOption !== null) { - $select .= ' ' . $selectOption; - } - - if (empty($columns)) { - return $select . ' *'; - } - - foreach ($columns as $i => $column) { - if ($column instanceof Expression) { - $columns[$i] = $column->expression; - $params = array_merge($params, $column->params); - } elseif (is_string($i)) { - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - $columns[$i] = "$column AS " . $this->db->quoteColumnName($i); - } elseif (strpos($column, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { - $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); - } else { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - } - - return $select . ' ' . implode(', ', $columns); - } - - /** - * @param array $tables - * @param array $params the binding parameters to be populated - * @return string the FROM clause built from [[Query::$from]]. - */ - public function buildFrom($tables, &$params) - { - if (empty($tables)) { - return ''; - } - - foreach ($tables as $i => $table) { - if ($table instanceof Query) { - list($sql, $params) = $this->build($table, $params); - $tables[$i] = "($sql) " . $this->db->quoteTableName($i); - } elseif (is_string($i)) { - if (strpos($table, '(') === false) { - $table = $this->db->quoteTableName($table); - } - $tables[$i] = "$table " . $this->db->quoteTableName($i); - } elseif (strpos($table, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias - $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); - } else { - $tables[$i] = $this->db->quoteTableName($table); - } - } - } - - return 'FROM ' . implode(', ', $tables); - } - - /** - * @param array $joins - * @param array $params the binding parameters to be populated - * @return string the JOIN clause built from [[Query::$join]]. - * @throws Exception if the $joins parameter is not in proper format - */ - public function buildJoin($joins, &$params) - { - if (empty($joins)) { - return ''; - } - - foreach ($joins as $i => $join) { - if (!is_array($join) || !isset($join[0], $join[1])) { - throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.'); - } - // 0:join type, 1:join table, 2:on-condition (optional) - list ($joinType, $table) = $join; - if (is_array($table)) { - $query = reset($table); - if (!$query instanceof Query) { - throw new Exception('The sub-query for join must be an instance of yii\db\Query.'); - } - $alias = $this->db->quoteTableName(key($table)); - list ($sql, $params) = $this->build($query, $params); - $table = "($sql) $alias"; - } elseif (strpos($table, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias - $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); - } else { - $table = $this->db->quoteTableName($table); - } - } - $joins[$i] = "$joinType $table"; - if (isset($join[2])) { - $condition = $this->buildCondition($join[2], $params); - if ($condition !== '') { - $joins[$i] .= ' ON ' . $condition; - } - } - } - - return implode($this->separator, $joins); - } - - /** - * @param string|array $condition - * @param array $params the binding parameters to be populated - * @return string the WHERE clause built from [[Query::$where]]. - */ - public function buildWhere($condition, &$params) - { - $where = $this->buildCondition($condition, $params); - return $where === '' ? '' : 'WHERE ' . $where; - } - - /** - * @param array $columns - * @return string the GROUP BY clause - */ - public function buildGroupBy($columns) - { - return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); - } - - /** - * @param string|array $condition - * @param array $params the binding parameters to be populated - * @return string the HAVING clause built from [[Query::$having]]. - */ - public function buildHaving($condition, &$params) - { - $having = $this->buildCondition($condition, $params); - return $having === '' ? '' : 'HAVING ' . $having; - } - - /** - * @param array $columns - * @return string the ORDER BY clause built from [[Query::$orderBy]]. - */ - public function buildOrderBy($columns) - { - if (empty($columns)) { - return ''; - } - $orders = []; - foreach ($columns as $name => $direction) { - if ($direction instanceof Expression) { - $orders[] = $direction->expression; - } else { - $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); - } - } - - return 'ORDER BY ' . implode(', ', $orders); - } - - /** - * @param integer $limit - * @param integer $offset - * @return string the LIMIT and OFFSET clauses built from [[Query::$limit]]. - */ - public function buildLimit($limit, $offset) - { - $sql = ''; - if ($this->hasLimit($limit)) { - $sql = 'LIMIT ' . $limit; - } - if ($this->hasOffset($offset)) { - $sql .= ' OFFSET ' . $offset; - } - return ltrim($sql); - } - - /** - * Checks to see if the given limit is effective. - * @param mixed $limit the given limit - * @return boolean whether the limit is effective - */ - protected function hasLimit($limit) - { - return is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0; - } - - /** - * Checks to see if the given offset is effective. - * @param mixed $offset the given offset - * @return boolean whether the offset is effective - */ - protected function hasOffset($offset) - { - return is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0'; - } - - /** - * @param array $unions - * @param array $params the binding parameters to be populated - * @return string the UNION clause built from [[Query::$union]]. - */ - public function buildUnion($unions, &$params) - { - if (empty($unions)) { - return ''; - } - - $result = ''; - - foreach ($unions as $i => $union) { - $query = $union['query']; - if ($query instanceof Query) { - list($unions[$i]['query'], $params) = $this->build($query, $params); - } - - $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) '; - } - - return trim($result); - } - - /** - * Processes columns and properly quote them if necessary. - * It will join all columns into a string with comma as separators. - * @param string|array $columns the columns to be processed - * @return string the processing result - */ - public function buildColumns($columns) - { - if (!is_array($columns)) { - if (strpos($columns, '(') !== false) { - return $columns; - } else { - $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); - } - } - foreach ($columns as $i => $column) { - if ($column instanceof Expression) { - $columns[$i] = $column->expression; - } elseif (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - return is_array($columns) ? implode(', ', $columns) : $columns; - } - - - /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if the condition is in bad format - */ - public function buildCondition($condition, &$params) - { - static $builders = [ - 'NOT' => 'buildNotCondition', - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'NOT LIKE' => 'buildLikeCondition', - 'OR LIKE' => 'buildLikeCondition', - 'OR NOT LIKE' => 'buildLikeCondition', - 'EXISTS' => 'buildExistsCondition', - 'NOT EXISTS' => 'buildExistsCondition', - ]; - - if (!is_array($condition)) { - return (string)$condition; - } elseif (empty($condition)) { - return ''; - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition, $params); - } else { - throw new InvalidParamException('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition, $params); - } - } - - /** - * Creates a condition based on column-value pairs. - * @param array $condition the condition specification. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - */ - public function buildHashCondition($condition, &$params) - { - $parts = []; - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('IN', [$column, $value], $params); - } else { - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - if ($value === null) { - $parts[] = "$column IS NULL"; - } elseif ($value instanceof Expression) { - $parts[] = "$column=" . $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $parts[] = "$column=$phName"; - $params[$phName] = $value; - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; - } - - /** - * Connects two or more SQL expressions with the `AND` or `OR` operator. - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the SQL expressions to connect. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - */ - public function buildAndCondition($operator, $operands, &$params) - { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - /** - * Inverts an SQL expressions with `NOT` operator. - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the SQL expressions to connect. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildNotCondition($operator, $operands, &$params) - { - if (count($operands) != 1) { - throw new InvalidParamException("Operator '$operator' requires exactly one operand."); - } - - $operand = reset($operands); - if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); - } - if ($operand === '') { - return ''; - } - return "$operator ($operand)"; - } - - /** - * Creates an SQL expressions with the `BETWEEN` operator. - * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) - * @param array $operands the first operand is the column name. The second and third operands - * describe the interval that column value should be in. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildBetweenCondition($operator, $operands, &$params) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new InvalidParamException("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - $phName1 = self::PARAM_PREFIX . count($params); - $params[$phName1] = $value1; - $phName2 = self::PARAM_PREFIX . count($params); - $params[$phName2] = $value2; - - return "$column $operator $phName1 AND $phName2"; - } - - /** - * Creates an SQL expressions with the `IN` operator. - * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) - * @param array $operands the first operand is the column name. If it is an array - * a composite IN condition will be generated. - * The second operand is an array of values that column value should be among. - * If it is an empty array the generated expression will be a `false` value if - * operator is `IN` and empty if operator is `NOT IN`. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws Exception if wrong number of operands have been given. - */ - public function buildInCondition($operator, $operands, &$params) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'IN' ? '0=1' : ''; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values, $params); - } elseif (is_array($column)) { - $column = reset($column); - } - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $values[$i] = 'NULL'; - } elseif ($value instanceof Expression) { - $values[$i] = $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value; - $values[$i] = $phName; - } - } - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - if (count($values) > 1) { - return "$column $operator (" . implode(', ', $values) . ')'; - } else { - $operator = $operator === 'IN' ? '=' : '<>'; - return $column . $operator . reset($values); - } - } - - protected function buildCompositeInCondition($operator, $columns, $values, &$params) - { - $vss = []; - foreach ($values as $value) { - $vs = []; - foreach ($columns as $column) { - if (isset($value[$column])) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value[$column]; - $vs[] = $phName; - } else { - $vs[] = 'NULL'; - } - } - $vss[] = '(' . implode(', ', $vs) . ')'; - } - foreach ($columns as $i => $column) { - if (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; - } - - /** - * Creates an SQL expressions with the `LIKE` operator. - * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) - * @param array $operands an array of two or three operands - * - * - The first operand is the column name. - * - The second operand is a single value or an array of values that column value - * should be compared with. If it is an empty array the generated expression will - * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator - * is `NOT LIKE` or `OR NOT LIKE`. - * - An optional third operand can also be provided to specify how to escape special characters - * in the value(s). The operand should be an array of mappings from the special characters to their - * escaped counterparts. If this operand is not provided, a default escape mapping will be used. - * You may use `false` or an empty array to indicate the values are already escaped and no escape - * should be applied. Note that when using an escape mapping (or the third operand is not provided), - * the values will be automatically enclosed within a pair of percentage characters. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. - */ - public function buildLikeCondition($operator, $operands, &$params) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']; - unset($operands[2]); - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values)) { - return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; - } - - if ($operator === 'LIKE' || $operator === 'NOT LIKE') { - $andor = ' AND '; - } else { - $andor = ' OR '; - $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; - } - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - $parts = []; - foreach ($values as $value) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); - $parts[] = "$column $operator $phName"; - } - - return implode($andor, $parts); - } - - /** - * Creates an SQL expressions with the `EXISTS` operator. - * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`) - * @param array $operands contains only one element which is a [[Query]] object representing the sub-query. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if the operand is not a [[Query]] object. - */ - public function buildExistsCondition($operator, $operands, &$params) - { - if ($operands[0] instanceof Query) { - list($sql, $params) = $this->build($operands[0], $params); - return "$operator ($sql)"; - } else { - throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.'); - } - } + /** + * The prefix for automatically generated query binding parameters. + */ + const PARAM_PREFIX = ':qp'; + + /** + * @var Connection the database connection. + */ + public $db; + /** + * @var string the separator between different fragments of a SQL statement. + * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement. + */ + public $separator = " "; + /** + * @var array the abstract column types mapped to physical column types. + * This is mainly used to support creating/modifying tables using DB-independent data type specifications. + * Child classes should override this property to declare supported type mappings. + */ + public $typeMap = []; + + /** + * Constructor. + * @param Connection $connection the database connection. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($connection, $config = []) + { + $this->db = $connection; + parent::__construct($config); + } + + /** + * Generates a SELECT SQL statement from a [[Query]] object. + * @param Query $query the [[Query]] object from which the SQL statement will be generated. + * @param array $params the parameters to be bound to the generated SQL statement. These parameters will + * be included in the result with the additional parameters generated during the query building process. + * @return array the generated SQL statement (the first array element) and the corresponding + * parameters to be bound to the SQL statement (the second array element). The parameters returned + * include those provided in `$params`. + */ + public function build($query, $params = []) + { + $params = empty($params) ? $query->params : array_merge($params, $query->params); + + $select = $query->select; + $from = $query->from; + if ($from === null && $query instanceof ActiveQuery) { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + $tableName = $modelClass::tableName(); + $from = [$tableName]; + if ($select === null && !empty($query->join)) { + $select = ["$tableName.*"]; + } + } + + $clauses = [ + $this->buildSelect($select, $params, $query->distinct, $query->selectOption), + $this->buildFrom($from, $params), + $this->buildJoin($query->join, $params), + $this->buildWhere($query->where, $params), + $this->buildGroupBy($query->groupBy), + $this->buildHaving($query->having, $params), + $this->buildOrderBy($query->orderBy), + $this->buildLimit($query->limit, $query->offset), + $this->buildUnion($query->union, $params), + ]; + + return [implode($this->separator, array_filter($clauses)), $params]; + } + + /** + * Creates an INSERT SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->insert('tbl_user', [ + * 'name' => 'Sam', + * 'age' => 30, + * ], $params); + * ~~~ + * + * The method will properly escape the table and column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name => value) to be inserted into the table. + * @param array $params the binding parameters that will be generated by this method. + * They should be bound to the DB command later. + * @return string the INSERT SQL + */ + public function insert($table, $columns, &$params) + { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + $names = []; + $placeholders = []; + foreach ($columns as $name => $value) { + $names[] = $this->db->quoteColumnName($name); + if ($value instanceof Expression) { + $placeholders[] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; + } + } + + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $names) . ') VALUES (' + . implode(', ', $placeholders) . ')'; + } + + /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->batchInsert('tbl_user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ]); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows) + { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } + + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } + $vs[] = is_string($value) ? $this->db->quoteValue($value) : ($value === null ? 'NULL' : $value); + } + $values[] = '(' . implode(', ', $vs) . ')'; + } + + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + } + + /** + * Creates an UPDATE SQL statement. + * For example, + * + * ~~~ + * $params = []; + * $sql = $queryBuilder->update('tbl_user', ['status' => 1], 'age > 30', $params); + * ~~~ + * + * The method will properly escape the table and column names. + * + * @param string $table the table to be updated. + * @param array $columns the column data (name => value) to be updated. + * @param array|string $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the DB command later. + * @return string the UPDATE SQL + */ + public function update($table, $columns, $condition, &$params) + { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + + $lines = []; + foreach ($columns as $name => $value) { + if ($value instanceof Expression) { + $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; + } + } + + $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); + $where = $this->buildWhere($condition, $params); + + return $where === '' ? $sql : $sql . ' ' . $where; + } + + /** + * Creates a DELETE SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->delete('tbl_user', 'status = 0'); + * ~~~ + * + * The method will properly escape the table and column names. + * + * @param string $table the table where the data will be deleted from. + * @param array|string $condition the condition that will be put in the WHERE part. Please + * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the DB command later. + * @return string the DELETE SQL + */ + public function delete($table, $condition, &$params) + { + $sql = 'DELETE FROM ' . $this->db->quoteTableName($table); + $where = $this->buildWhere($condition, $params); + + return $where === '' ? $sql : $sql . ' ' . $where; + } + + /** + * Builds a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * For example, + * + * ~~~ + * $sql = $queryBuilder->createTable('tbl_user', [ + * 'id' => 'pk', + * 'name' => 'string', + * 'age' => 'integer', + * ]); + * ~~~ + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name => definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + * @return string the SQL statement for creating a new DB table. + */ + public function createTable($table, $columns, $options = null) + { + $cols = []; + foreach ($columns as $name => $type) { + if (is_string($name)) { + $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type); + } else { + $cols[] = "\t" . $type; + } + } + $sql = "CREATE TABLE " . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)"; + + return $options === null ? $sql : $sql . ' ' . $options; + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $oldName the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($oldName, $newName) + { + return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName); + } + + /** + * Builds a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a DB table. + */ + public function dropTable($table) + { + return "DROP TABLE " . $this->db->quoteTableName($table); + } + + /** + * Builds a SQL statement for adding a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * @return string the SQL statement for adding a primary key constraint to an existing table. + */ + public function addPrimaryKey($name, $table, $columns) + { + if (is_string($columns)) { + $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + } + + foreach ($columns as $i => $col) { + $columns[$i] = $this->db->quoteColumnName($col); + } + + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT ' + . $this->db->quoteColumnName($name) . ' PRIMARY KEY (' + . implode(', ', $columns). ' )'; + } + + /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return string the SQL statement for removing a primary key constraint from an existing table. * + */ + public function dropPrimaryKey($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); + } + + /** + * Builds a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return string the SQL statement for truncating a DB table. + */ + public function truncateTable($table) + { + return "TRUNCATE TABLE " . $this->db->quoteTableName($table); + } + + /** + * Builds a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for adding a new column. + */ + public function addColumn($table, $column, $type) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' ADD ' . $this->db->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + } + + /** + * Builds a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a DB column. + */ + public function dropColumn($table, $column) + { + return "ALTER TABLE " . $this->db->quoteTableName($table) + . " DROP COLUMN " . $this->db->quoteColumnName($column); + } + + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $oldName the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + */ + public function renameColumn($table, $oldName, $newName) + { + return "ALTER TABLE " . $this->db->quoteTableName($table) + . " RENAME COLUMN " . $this->db->quoteColumnName($oldName) + . " TO " . $this->db->quoteColumnName($newName); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract + * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept + * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' + * will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE ' + . $this->db->quoteColumnName($column) . ' ' + . $this->db->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string|array $columns the name of the column to that the constraint will be added on. + * If there are multiple columns, separate them with commas or use an array to represent them. + * @param string $refTable the table that the foreign key references to. + * @param string|array $refColumns the name of the column that the foreign key references to. + * If there are multiple columns, separate them with commas or use an array to represent them. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return string the SQL statement for adding a foreign key constraint to an existing table. + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name) + . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')' + . ' REFERENCES ' . $this->db->quoteTableName($refTable) + . ' (' . $this->buildColumns($refColumns) . ')'; + if ($delete !== null) { + $sql .= ' ON DELETE ' . $delete; + } + if ($update !== null) { + $sql .= ' ON UPDATE ' . $update; + } + + return $sql; + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a foreign key constraint. + */ + public function dropForeignKey($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name); + } + + /** + * Builds a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns, + * separate them with commas or use an array to represent them. Each column name will be properly quoted + * by the method, unless a parenthesis is found in the name. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + * @return string the SQL statement for creating a new index. + */ + public function createIndex($name, $table, $columns, $unique = false) + { + return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') + . $this->db->quoteTableName($name) . ' ON ' + . $this->db->quoteTableName($table) + . ' (' . $this->buildColumns($columns) . ')'; + } + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table); + } + + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $table the name of the table whose primary key sequence will be reset + * @param array|string $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return string the SQL statement for resetting sequence + * @throws NotSupportedException if this is not supported by the underlying DBMS + */ + public function resetSequence($table, $value = null) + { + throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.'); + } + + /** + * Builds a SQL statement for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. + * @return string the SQL statement for checking integrity + * @throws NotSupportedException if this is not supported by the underlying DBMS + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.'); + } + + /** + * Converts an abstract column type into a physical column type. + * The conversion is done using the type map specified in [[typeMap]]. + * The following abstract column types are supported (using MySQL as an example to explain the corresponding + * physical types): + * + * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY" + * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY" + * - `string`: string type, will be converted into "varchar(255)" + * - `text`: a long string type, will be converted into "text" + * - `smallint`: a small integer type, will be converted into "smallint(6)" + * - `integer`: integer type, will be converted into "int(11)" + * - `bigint`: a big integer type, will be converted into "bigint(20)" + * - `boolean`: boolean type, will be converted into "tinyint(1)" + * - `float``: float number type, will be converted into "float" + * - `decimal`: decimal number type, will be converted into "decimal" + * - `datetime`: datetime type, will be converted into "datetime" + * - `timestamp`: timestamp type, will be converted into "timestamp" + * - `time`: time type, will be converted into "time" + * - `date`: date type, will be converted into "date" + * - `money`: money type, will be converted into "decimal(19,4)" + * - `binary`: binary data type, will be converted into "blob" + * + * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only + * the first part will be converted, and the rest of the parts will be appended to the converted result. + * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. + * + * For some of the abstract types you can also specify a length or precision constraint + * by prepending it in round brackets directly to the type. + * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. + * If the underlying DBMS does not support these kind of constraints for a type it will + * be ignored. + * + * If a type cannot be found in [[typeMap]], it will be returned without any change. + * @param string $type abstract column type + * @return string physical column type. + */ + public function getColumnType($type) + { + if (isset($this->typeMap[$type])) { + return $this->typeMap[$type]; + } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3]; + } + } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); + } + } + + return $type; + } + + /** + * @param array $columns + * @param array $params the binding parameters to be populated + * @param boolean $distinct + * @param string $selectOption + * @return string the SELECT clause built from [[Query::$select]]. + */ + public function buildSelect($columns, &$params, $distinct = false, $selectOption = null) + { + $select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; + if ($selectOption !== null) { + $select .= ' ' . $selectOption; + } + + if (empty($columns)) { + return $select . ' *'; + } + + foreach ($columns as $i => $column) { + if ($column instanceof Expression) { + $columns[$i] = $column->expression; + $params = array_merge($params, $column->params); + } elseif (is_string($i)) { + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $columns[$i] = "$column AS " . $this->db->quoteColumnName($i); + } elseif (strpos($column, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { + $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); + } else { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + } + + return $select . ' ' . implode(', ', $columns); + } + + /** + * @param array $tables + * @param array $params the binding parameters to be populated + * @return string the FROM clause built from [[Query::$from]]. + */ + public function buildFrom($tables, &$params) + { + if (empty($tables)) { + return ''; + } + + foreach ($tables as $i => $table) { + if ($table instanceof Query) { + list($sql, $params) = $this->build($table, $params); + $tables[$i] = "($sql) " . $this->db->quoteTableName($i); + } elseif (is_string($i)) { + if (strpos($table, '(') === false) { + $table = $this->db->quoteTableName($table); + } + $tables[$i] = "$table " . $this->db->quoteTableName($i); + } elseif (strpos($table, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias + $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); + } else { + $tables[$i] = $this->db->quoteTableName($table); + } + } + } + + return 'FROM ' . implode(', ', $tables); + } + + /** + * @param array $joins + * @param array $params the binding parameters to be populated + * @return string the JOIN clause built from [[Query::$join]]. + * @throws Exception if the $joins parameter is not in proper format + */ + public function buildJoin($joins, &$params) + { + if (empty($joins)) { + return ''; + } + + foreach ($joins as $i => $join) { + if (!is_array($join) || !isset($join[0], $join[1])) { + throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.'); + } + // 0:join type, 1:join table, 2:on-condition (optional) + list ($joinType, $table) = $join; + if (is_array($table)) { + $query = reset($table); + if (!$query instanceof Query) { + throw new Exception('The sub-query for join must be an instance of yii\db\Query.'); + } + $alias = $this->db->quoteTableName(key($table)); + list ($sql, $params) = $this->build($query, $params); + $table = "($sql) $alias"; + } elseif (strpos($table, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias + $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); + } else { + $table = $this->db->quoteTableName($table); + } + } + $joins[$i] = "$joinType $table"; + if (isset($join[2])) { + $condition = $this->buildCondition($join[2], $params); + if ($condition !== '') { + $joins[$i] .= ' ON ' . $condition; + } + } + } + + return implode($this->separator, $joins); + } + + /** + * @param string|array $condition + * @param array $params the binding parameters to be populated + * @return string the WHERE clause built from [[Query::$where]]. + */ + public function buildWhere($condition, &$params) + { + $where = $this->buildCondition($condition, $params); + + return $where === '' ? '' : 'WHERE ' . $where; + } + + /** + * @param array $columns + * @return string the GROUP BY clause + */ + public function buildGroupBy($columns) + { + return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); + } + + /** + * @param string|array $condition + * @param array $params the binding parameters to be populated + * @return string the HAVING clause built from [[Query::$having]]. + */ + public function buildHaving($condition, &$params) + { + $having = $this->buildCondition($condition, $params); + + return $having === '' ? '' : 'HAVING ' . $having; + } + + /** + * @param array $columns + * @return string the ORDER BY clause built from [[Query::$orderBy]]. + */ + public function buildOrderBy($columns) + { + if (empty($columns)) { + return ''; + } + $orders = []; + foreach ($columns as $name => $direction) { + if ($direction instanceof Expression) { + $orders[] = $direction->expression; + } else { + $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); + } + } + + return 'ORDER BY ' . implode(', ', $orders); + } + + /** + * @param integer $limit + * @param integer $offset + * @return string the LIMIT and OFFSET clauses built from [[Query::$limit]]. + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + if ($this->hasLimit($limit)) { + $sql = 'LIMIT ' . $limit; + } + if ($this->hasOffset($offset)) { + $sql .= ' OFFSET ' . $offset; + } + + return ltrim($sql); + } + + /** + * Checks to see if the given limit is effective. + * @param mixed $limit the given limit + * @return boolean whether the limit is effective + */ + protected function hasLimit($limit) + { + return is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0; + } + + /** + * Checks to see if the given offset is effective. + * @param mixed $offset the given offset + * @return boolean whether the offset is effective + */ + protected function hasOffset($offset) + { + return is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0'; + } + + /** + * @param array $unions + * @param array $params the binding parameters to be populated + * @return string the UNION clause built from [[Query::$union]]. + */ + public function buildUnion($unions, &$params) + { + if (empty($unions)) { + return ''; + } + + $result = ''; + + foreach ($unions as $i => $union) { + $query = $union['query']; + if ($query instanceof Query) { + list($unions[$i]['query'], $params) = $this->build($query, $params); + } + + $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) '; + } + + return trim($result); + } + + /** + * Processes columns and properly quote them if necessary. + * It will join all columns into a string with comma as separators. + * @param string|array $columns the columns to be processed + * @return string the processing result + */ + public function buildColumns($columns) + { + if (!is_array($columns)) { + if (strpos($columns, '(') !== false) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY); + } + } + foreach ($columns as $i => $column) { + if ($column instanceof Expression) { + $columns[$i] = $column->expression; + } elseif (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + + return is_array($columns) ? implode(', ', $columns) : $columns; + } + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if the condition is in bad format + */ + public function buildCondition($condition, &$params) + { + static $builders = [ + 'NOT' => 'buildNotCondition', + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', + 'EXISTS' => 'buildExistsCondition', + 'NOT EXISTS' => 'buildExistsCondition', + ]; + + if (!is_array($condition)) { + return (string) $condition; + } elseif (empty($condition)) { + return ''; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + + return $this->$method($operator, $condition, $params); + } else { + throw new InvalidParamException('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + + return $this->buildHashCondition($condition, $params); + } + } + + /** + * Creates a condition based on column-value pairs. + * @param array $condition the condition specification. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildHashCondition($condition, &$params) + { + $parts = []; + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('IN', [$column, $value], $params); + } else { + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + if ($value === null) { + $parts[] = "$column IS NULL"; + } elseif ($value instanceof Expression) { + $parts[] = "$column=" . $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$column=$phName"; + $params[$phName] = $value; + } + } + } + + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + /** + * Connects two or more SQL expressions with the `AND` or `OR` operator. + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the SQL expressions to connect. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildAndCondition($operator, $operands, &$params) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + /** + * Inverts an SQL expressions with `NOT` operator. + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the SQL expressions to connect. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildNotCondition($operator, $operands, &$params) + { + if (count($operands) != 1) { + throw new InvalidParamException("Operator '$operator' requires exactly one operand."); + } + + $operand = reset($operands); + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand === '') { + return ''; + } + + return "$operator ($operand)"; + } + + /** + * Creates an SQL expressions with the `BETWEEN` operator. + * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) + * @param array $operands the first operand is the column name. The second and third operands + * describe the interval that column value should be in. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildBetweenCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new InvalidParamException("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $phName1 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName2] = $value2; + + return "$column $operator $phName1 AND $phName2"; + } + + /** + * Creates an SQL expressions with the `IN` operator. + * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) + * @param array $operands the first operand is the column name. If it is an array + * a composite IN condition will be generated. + * The second operand is an array of values that column value should be among. + * If it is an empty array the generated expression will be a `false` value if + * operator is `IN` and empty if operator is `NOT IN`. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws Exception if wrong number of operands have been given. + */ + public function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array) $values; + + if (empty($values) || $column === []) { + return $operator === 'IN' ? '0=1' : ''; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; + } elseif ($value instanceof Expression) { + $values[$i] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = $phName; + } + } + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + + return $column . $operator . reset($values); + } + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($columns as $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $phName; + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + /** + * Creates an SQL expressions with the `LIKE` operator. + * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`) + * @param array $operands an array of two or three operands + * + * - The first operand is the column name. + * - The second operand is a single value or an array of values that column value + * should be compared with. If it is an empty array the generated expression will + * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator + * is `NOT LIKE` or `OR NOT LIKE`. + * - An optional third operand can also be provided to specify how to escape special characters + * in the value(s). The operand should be an array of mappings from the special characters to their + * escaped counterparts. If this operand is not provided, a default escape mapping will be used. + * You may use `false` or an empty array to indicate the values are already escaped and no escape + * should be applied. Note that when using an escape mapping (or the third operand is not provided), + * the values will be automatically enclosed within a pair of percentage characters. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildLikeCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\']; + unset($operands[2]); + + list($column, $values) = $operands; + + $values = (array) $values; + + if (empty($values)) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = []; + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } + + /** + * Creates an SQL expressions with the `EXISTS` operator. + * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`) + * @param array $operands contains only one element which is a [[Query]] object representing the sub-query. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if the operand is not a [[Query]] object. + */ + public function buildExistsCondition($operator, $operands, &$params) + { + if ($operands[0] instanceof Query) { + list($sql, $params) = $this->build($operands[0], $params); + + return "$operator ($sql)"; + } else { + throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.'); + } + } } diff --git a/framework/db/QueryInterface.php b/framework/db/QueryInterface.php index bc0dab4e5a0..8bfac52cdae 100644 --- a/framework/db/QueryInterface.php +++ b/framework/db/QueryInterface.php @@ -22,184 +22,184 @@ */ interface QueryInterface { - /** - * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null); + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null); - /** - * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - public function one($db = null); + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null); - /** - * Returns the number of records. - * @param string $q the COUNT expression. Defaults to '*'. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null); + /** + * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function count($q = '*', $db = null); - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. - * @return boolean whether the query result contains any row of data. - */ - public function exists($db = null); + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null); - /** - * Sets the [[indexBy]] property. - * @param string|callable $column the name of the column by which the query results should be indexed by. - * This can also be a callable (e.g. anonymous function) that returns the index value based on the given - * row data. The signature of the callable should be: - * - * ~~~ - * function ($row) - * { - * // return the index value corresponding to $row - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column); + /** + * Sets the [[indexBy]] property. + * @param string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column); - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter. - * - * The $condition parameter should be an array in one of the following two formats: - * - * - hash format: `['column1' => value1, 'column2' => value2, ...]` - * - operator format: `[operator, operand1, operand2, ...]` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. - * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. - * - `['status' => null] generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape special characters in the values. - * Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply - * a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. - * - * @param array $condition the conditions that should be put in the WHERE part. - * @return static the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition); + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter. + * + * The $condition parameter should be an array in one of the following two formats: + * + * - hash format: `['column1' => value1, 'column2' => value2, ...]` + * - operator format: `[operator, operand1, operand2, ...]` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. + * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `['status' => null] generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `['like', 'name', 'tester']` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `['like', 'name', ['test', 'sample']]` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape special characters in the values. + * Sometimes, you may want to add the percentage characters to the matching value by yourself, you may supply + * a third operand `false` to do so. For example, `['like', 'name', '%tester', false]` will generate `name LIKE '%tester'`. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param array $condition the conditions that should be put in the WHERE part. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition); - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition); + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition); - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition); + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition); - /** - * Sets the ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see addOrderBy() - */ - public function orderBy($columns); + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addOrderBy() + */ + public function orderBy($columns); - /** - * Adds additional ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see orderBy() - */ - public function addOrderBy($columns); + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see orderBy() + */ + public function addOrderBy($columns); - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit. Use null or negative value to disable limit. - * @return static the query object itself - */ - public function limit($limit); + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return static the query object itself + */ + public function limit($limit); - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset. Use null or negative value to disable offset. - * @return static the query object itself - */ - public function offset($offset); + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return static the query object itself + */ + public function offset($offset); } diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php index e4184eacd9f..363aa5f33f0 100644 --- a/framework/db/QueryTrait.php +++ b/framework/db/QueryTrait.php @@ -18,189 +18,198 @@ */ trait QueryTrait { - /** - * @var string|array query condition. This refers to the WHERE clause in a SQL statement. - * For example, `age > 31 AND team = 1`. - * @see where() - */ - public $where; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. - */ - public $limit; - /** - * @var integer zero-based offset from where the records are to be returned. If not set or - * less than 0, it means starting from the beginning. - */ - public $offset; - /** - * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. - * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which - * can be either [SORT_ASC](http://php.net/manual/en/array.constants.php#constant.sort-asc) - * or [SORT_DESC](http://php.net/manual/en/array.constants.php#constant.sort-desc). - * The array may also contain [[Expression]] objects. If that is the case, the expressions - * will be converted into strings without any change. - */ - public $orderBy; - /** - * @var string|callable $column the name of the column by which the query results should be indexed by. - * This can also be a callable (e.g. anonymous function) that returns the index value based on the given - * row data. For more details, see [[indexBy()]]. This property is only used by [[QueryInterface::all()|all()]]. - */ - public $indexBy; - - /** - * Sets the [[indexBy]] property. - * @param string|callable $column the name of the column by which the query results should be indexed by. - * This can also be a callable (e.g. anonymous function) that returns the index value based on the given - * row data. The signature of the callable should be: - * - * ~~~ - * function ($row) - * { - * // return the index value corresponding to $row - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column) - { - $this->indexBy = $column; - return $this; - } - - /** - * Sets the WHERE part of the query. - * - * See [[QueryInterface::where()]] for detailed documentation. - * - * @param array $condition the conditions that should be put in the WHERE part. - * @return static the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition) - { - $this->where = $condition; - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['and', $this->where, $condition]; - } - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['or', $this->where, $condition]; - } - return $this; - } - - /** - * Sets the ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * Note that if your order-by is an expression containing commas, you should always use an array - * to represent the order-by information. Otherwise, the method will not be able to correctly determine - * the order-by columns. - * @return static the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->orderBy = $this->normalizeOrderBy($columns); - return $this; - } - - /** - * Adds additional ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see orderBy() - */ - public function addOrderBy($columns) - { - $columns = $this->normalizeOrderBy($columns); - if ($this->orderBy === null) { - $this->orderBy = $columns; - } else { - $this->orderBy = array_merge($this->orderBy, $columns); - } - return $this; - } - - protected function normalizeOrderBy($columns) - { - if (is_array($columns)) { - return $columns; - } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - $result = []; - foreach ($columns as $column) { - if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC; - } else { - $result[$column] = SORT_ASC; - } - } - return $result; - } - } - - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit. Use null or negative value to disable limit. - * @return static the query object itself - */ - public function limit($limit) - { - $this->limit = $limit; - return $this; - } - - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset. Use null or negative value to disable offset. - * @return static the query object itself - */ - public function offset($offset) - { - $this->offset = $offset; - return $this; - } + /** + * @var string|array query condition. This refers to the WHERE clause in a SQL statement. + * For example, `age > 31 AND team = 1`. + * @see where() + */ + public $where; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. If not set or + * less than 0, it means starting from the beginning. + */ + public $offset; + /** + * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which + * can be either [SORT_ASC](http://php.net/manual/en/array.constants.php#constant.sort-asc) + * or [SORT_DESC](http://php.net/manual/en/array.constants.php#constant.sort-desc). + * The array may also contain [[Expression]] objects. If that is the case, the expressions + * will be converted into strings without any change. + */ + public $orderBy; + /** + * @var string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. For more details, see [[indexBy()]]. This property is only used by [[QueryInterface::all()|all()]]. + */ + public $indexBy; + + /** + * Sets the [[indexBy]] property. + * @param string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column) + { + $this->indexBy = $column; + + return $this; + } + + /** + * Sets the WHERE part of the query. + * + * See [[QueryInterface::where()]] for detailed documentation. + * + * @param array $condition the conditions that should be put in the WHERE part. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition) + { + $this->where = $condition; + + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['and', $this->where, $condition]; + } + + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['or', $this->where, $condition]; + } + + return $this; + } + + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * Note that if your order-by is an expression containing commas, you should always use an array + * to represent the order-by information. Otherwise, the method will not be able to correctly determine + * the order-by columns. + * @return static the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->orderBy = $this->normalizeOrderBy($columns); + + return $this; + } + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { + $this->orderBy = $columns; + } else { + $this->orderBy = array_merge($this->orderBy, $columns); + } + + return $this; + } + + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = []; + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC; + } else { + $result[$column] = SORT_ASC; + } + } + + return $result; + } + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return static the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return static the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + + return $this; + } } diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 1539775f569..79625c93a94 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -31,430 +31,439 @@ */ abstract class Schema extends Object { - /** - * The followings are the supported abstract column data types. - */ - const TYPE_PK = 'pk'; - const TYPE_BIGPK = 'bigpk'; - const TYPE_STRING = 'string'; - const TYPE_TEXT = 'text'; - const TYPE_SMALLINT = 'smallint'; - const TYPE_INTEGER = 'integer'; - const TYPE_BIGINT = 'bigint'; - const TYPE_FLOAT = 'float'; - const TYPE_DECIMAL = 'decimal'; - const TYPE_DATETIME = 'datetime'; - const TYPE_TIMESTAMP = 'timestamp'; - const TYPE_TIME = 'time'; - const TYPE_DATE = 'date'; - const TYPE_BINARY = 'binary'; - const TYPE_BOOLEAN = 'boolean'; - const TYPE_MONEY = 'money'; - - /** - * @var Connection the database connection - */ - public $db; - /** - * @var string the default schema name used for the current session. - */ - public $defaultSchema; - /** - * @var array list of ALL table names in the database - */ - private $_tableNames = []; - /** - * @var array list of loaded table metadata (table name => TableSchema) - */ - private $_tables = []; - /** - * @var QueryBuilder the query builder for this database - */ - private $_builder; - - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema DBMS-dependent table metadata, null if the table does not exist. - */ - abstract protected function loadTableSchema($name); - - - /** - * Obtains the metadata for the named table. - * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. - * @param boolean $refresh whether to reload the table schema even if it is found in the cache. - * @return TableSchema table metadata. Null if the named table does not exist. - */ - public function getTableSchema($name, $refresh = false) - { - if (isset($this->_tables[$name]) && !$refresh) { - return $this->_tables[$name]; - } - - $db = $this->db; - $realName = $this->getRawTableName($name); - - if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { - /** @var Cache $cache */ - $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; - if ($cache instanceof Cache) { - $key = $this->getCacheKey($name); - if ($refresh || ($table = $cache->get($key)) === false) { - $table = $this->loadTableSchema($realName); - if ($table !== null) { - $cache->set($key, $table, $db->schemaCacheDuration, new GroupDependency([ - 'group' => $this->getCacheGroup(), - ])); - } - } - return $this->_tables[$name] = $table; - } - } - return $this->_tables[$name] = $this->loadTableSchema($realName); - } - - /** - * Returns the cache key for the specified table name. - * @param string $name the table name - * @return mixed the cache key - */ - protected function getCacheKey($name) - { - return [ - __CLASS__, - $this->db->dsn, - $this->db->username, - $name, - ]; - } - - /** - * Returns the cache group name. - * This allows [[refresh()]] to invalidate all cached table schemas. - * @return string the cache group name - */ - protected function getCacheGroup() - { - return md5(serialize([ - __CLASS__, - $this->db->dsn, - $this->db->username, - ])); - } - - /** - * Returns the metadata for all tables in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. - * @param boolean $refresh whether to fetch the latest available table schemas. If this is false, - * cached data may be returned if available. - * @return TableSchema[] the metadata for all tables in the database. - * Each array element is an instance of [[TableSchema]] or its child class. - */ - public function getTableSchemas($schema = '', $refresh = false) - { - $tables = []; - foreach ($this->getTableNames($schema, $refresh) as $name) { - if ($schema !== '') { - $name = $schema . '.' . $name; - } - if (($table = $this->getTableSchema($name, $refresh)) !== null) { - $tables[] = $table; - } - } - return $tables; - } - - /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. - * If not empty, the returned table names will be prefixed with the schema name. - * @param boolean $refresh whether to fetch the latest available table names. If this is false, - * table names fetched previously (if available) will be returned. - * @return string[] all table names in the database. - */ - public function getTableNames($schema = '', $refresh = false) - { - if (!isset($this->_tableNames[$schema]) || $refresh) { - $this->_tableNames[$schema] = $this->findTableNames($schema); - } - return $this->_tableNames[$schema]; - } - - /** - * @return QueryBuilder the query builder for this connection. - */ - public function getQueryBuilder() - { - if ($this->_builder === null) { - $this->_builder = $this->createQueryBuilder(); - } - return $this->_builder; - } - - /** - * Determines the PDO type for the given PHP data value. - * @param mixed $data the data whose PDO type is to be determined - * @return integer the PDO type - * @see http://www.php.net/manual/en/pdo.constants.php - */ - public function getPdoType($data) - { - static $typeMap = [ - // php type => PDO type - 'boolean' => \PDO::PARAM_BOOL, - 'integer' => \PDO::PARAM_INT, - 'string' => \PDO::PARAM_STR, - 'resource' => \PDO::PARAM_LOB, - 'NULL' => \PDO::PARAM_NULL, - ]; - $type = gettype($data); - return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; - } - - /** - * Refreshes the schema. - * This method cleans up all cached table schemas so that they can be re-created later - * to reflect the database schema change. - */ - public function refresh() - { - /** @var Cache $cache */ - $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; - if ($this->db->enableSchemaCache && $cache instanceof Cache) { - GroupDependency::invalidate($cache, $this->getCacheGroup()); - } - $this->_tableNames = []; - $this->_tables = []; - } - - /** - * Creates a query builder for the database. - * This method may be overridden by child classes to create a DBMS-specific query builder. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Returns all table names in the database. - * This method should be overridden by child classes in order to support this feature - * because the default implementation simply throws an exception. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - * @throws NotSupportedException if this method is called - */ - protected function findTableNames($schema = '') - { - throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.'); - } - - /** - * Returns all unique indexes for the given table. - * Each array element is of the following structure: - * - * ~~~ - * [ - * 'IndexName1' => ['col1' [, ...]], - * 'IndexName2' => ['col2' [, ...]], - * ] - * ~~~ - * - * This method should be overridden by child classes in order to support this feature - * because the default implementation simply throws an exception - * @param TableSchema $table the table metadata - * @return array all unique indexes for the given table. - * @throws NotSupportedException if this method is called - */ - public function findUniqueIndexes($table) - { - throw new NotSupportedException(get_class($this) . ' does not support getting unique indexes information.'); - } - - /** - * Returns the ID of the last inserted row or sequence value. - * @param string $sequenceName name of the sequence object (required by some DBMS) - * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object - * @throws InvalidCallException if the DB connection is not active - * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php - */ - public function getLastInsertID($sequenceName = '') - { - if ($this->db->isActive) { - return $this->db->pdo->lastInsertId($sequenceName); - } else { - throw new InvalidCallException('DB Connection is not active.'); - } - } - - /** - * @return boolean whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint). - */ - public function supportsSavepoint() - { - return $this->db->enableSavepoint; - } - - /** - * Creates a new savepoint. - * @param string $name the savepoint name - */ - public function createSavepoint($name) - { - $this->db->createCommand("SAVEPOINT $name")->execute(); - } - - /** - * Releases an existing savepoint. - * @param string $name the savepoint name - */ - public function releaseSavepoint($name) - { - $this->db->createCommand("RELEASE SAVEPOINT $name")->execute(); - } - - /** - * Rolls back to a previously created savepoint. - * @param string $name the savepoint name - */ - public function rollBackSavepoint($name) - { - $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute(); - } - - /** - * Quotes a string value for use in a query. - * Note that if the parameter is not a string, it will be returned without change. - * @param string $str string to be quoted - * @return string the properly quoted string - * @see http://www.php.net/manual/en/function.PDO-quote.php - */ - public function quoteValue($str) - { - if (!is_string($str)) { - return $str; - } - - $this->db->open(); - if (($value = $this->db->pdo->quote($str)) !== false) { - return $value; - } else { // the driver doesn't support quote (e.g. oci) - return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; - } - } - - /** - * Quotes a table name for use in a query. - * If the table name contains schema prefix, the prefix will also be properly quoted. - * If the table name is already quoted or contains '(' or '{{', - * then this method will do nothing. - * @param string $name table name - * @return string the properly quoted table name - * @see quoteSimpleTableName() - */ - public function quoteTableName($name) - { - if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { - return $name; - } - if (strpos($name, '.') === false) { - return $this->quoteSimpleTableName($name); - } - $parts = explode('.', $name); - foreach ($parts as $i => $part) { - $parts[$i] = $this->quoteSimpleTableName($part); - } - return implode('.', $parts); - - } - - /** - * Quotes a column name for use in a query. - * If the column name contains prefix, the prefix will also be properly quoted. - * If the column name is already quoted or contains '(', '[[' or '{{', - * then this method will do nothing. - * @param string $name column name - * @return string the properly quoted column name - * @see quoteSimpleColumnName() - */ - public function quoteColumnName($name) - { - if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { - return $name; - } - if (($pos = strrpos($name, '.')) !== false) { - $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.'; - $name = substr($name, $pos + 1); - } else { - $prefix = ''; - } - return $prefix . $this->quoteSimpleColumnName($name); - } - - /** - * Quotes a simple table name for use in a query. - * A simple table name should contain the table name only without any schema prefix. - * If the table name is already quoted, this method will do nothing. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteSimpleTableName($name) - { - return strpos($name, "'") !== false ? $name : "'" . $name . "'"; - } - - /** - * Quotes a simple column name for use in a query. - * A simple column name should contain the column name only without any prefix. - * If the column name is already quoted or is the asterisk character '*', this method will do nothing. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; - } - - /** - * Returns the actual name of a given table name. - * This method will strip off curly brackets from the given table name - * and replace the percentage character '%' with [[Connection::tablePrefix]]. - * @param string $name the table name to be converted - * @return string the real name of the given table name - */ - public function getRawTableName($name) - { - if (strpos($name, '{{') !== false) { - $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); - return str_replace('%', $this->db->tablePrefix, $name); - } else { - return $name; - } - } - - /** - * Extracts the PHP type from abstract DB type. - * @param ColumnSchema $column the column schema information - * @return string PHP type name - */ - protected function getColumnPhpType($column) - { - static $typeMap = [ // abstract type => php type - 'smallint' => 'integer', - 'integer' => 'integer', - 'boolean' => 'boolean', - 'float' => 'double', - ]; - if (isset($typeMap[$column->type])) { - if ($column->type === 'integer') { - return $column->unsigned ? 'string' : 'integer'; - } else { - return $typeMap[$column->type]; - } - } else { - return 'string'; - } - } + /** + * The followings are the supported abstract column data types. + */ + const TYPE_PK = 'pk'; + const TYPE_BIGPK = 'bigpk'; + const TYPE_STRING = 'string'; + const TYPE_TEXT = 'text'; + const TYPE_SMALLINT = 'smallint'; + const TYPE_INTEGER = 'integer'; + const TYPE_BIGINT = 'bigint'; + const TYPE_FLOAT = 'float'; + const TYPE_DECIMAL = 'decimal'; + const TYPE_DATETIME = 'datetime'; + const TYPE_TIMESTAMP = 'timestamp'; + const TYPE_TIME = 'time'; + const TYPE_DATE = 'date'; + const TYPE_BINARY = 'binary'; + const TYPE_BOOLEAN = 'boolean'; + const TYPE_MONEY = 'money'; + + /** + * @var Connection the database connection + */ + public $db; + /** + * @var string the default schema name used for the current session. + */ + public $defaultSchema; + /** + * @var array list of ALL table names in the database + */ + private $_tableNames = []; + /** + * @var array list of loaded table metadata (table name => TableSchema) + */ + private $_tables = []; + /** + * @var QueryBuilder the query builder for this database + */ + private $_builder; + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema DBMS-dependent table metadata, null if the table does not exist. + */ + abstract protected function loadTableSchema($name); + + /** + * Obtains the metadata for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param boolean $refresh whether to reload the table schema even if it is found in the cache. + * @return TableSchema table metadata. Null if the named table does not exist. + */ + public function getTableSchema($name, $refresh = false) + { + if (isset($this->_tables[$name]) && !$refresh) { + return $this->_tables[$name]; + } + + $db = $this->db; + $realName = $this->getRawTableName($name); + + if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { + /** @var Cache $cache */ + $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; + if ($cache instanceof Cache) { + $key = $this->getCacheKey($name); + if ($refresh || ($table = $cache->get($key)) === false) { + $table = $this->loadTableSchema($realName); + if ($table !== null) { + $cache->set($key, $table, $db->schemaCacheDuration, new GroupDependency([ + 'group' => $this->getCacheGroup(), + ])); + } + } + + return $this->_tables[$name] = $table; + } + } + + return $this->_tables[$name] = $this->loadTableSchema($realName); + } + + /** + * Returns the cache key for the specified table name. + * @param string $name the table name + * @return mixed the cache key + */ + protected function getCacheKey($name) + { + return [ + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + ]; + } + + /** + * Returns the cache group name. + * This allows [[refresh()]] to invalidate all cached table schemas. + * @return string the cache group name + */ + protected function getCacheGroup() + { + return md5(serialize([ + __CLASS__, + $this->db->dsn, + $this->db->username, + ])); + } + + /** + * Returns the metadata for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * @param boolean $refresh whether to fetch the latest available table schemas. If this is false, + * cached data may be returned if available. + * @return TableSchema[] the metadata for all tables in the database. + * Each array element is an instance of [[TableSchema]] or its child class. + */ + public function getTableSchemas($schema = '', $refresh = false) + { + $tables = []; + foreach ($this->getTableNames($schema, $refresh) as $name) { + if ($schema !== '') { + $name = $schema . '.' . $name; + } + if (($table = $this->getTableSchema($name, $refresh)) !== null) { + $tables[] = $table; + } + } + + return $tables; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name. + * If not empty, the returned table names will be prefixed with the schema name. + * @param boolean $refresh whether to fetch the latest available table names. If this is false, + * table names fetched previously (if available) will be returned. + * @return string[] all table names in the database. + */ + public function getTableNames($schema = '', $refresh = false) + { + if (!isset($this->_tableNames[$schema]) || $refresh) { + $this->_tableNames[$schema] = $this->findTableNames($schema); + } + + return $this->_tableNames[$schema]; + } + + /** + * @return QueryBuilder the query builder for this connection. + */ + public function getQueryBuilder() + { + if ($this->_builder === null) { + $this->_builder = $this->createQueryBuilder(); + } + + return $this->_builder; + } + + /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + static $typeMap = [ + // php type => PDO type + 'boolean' => \PDO::PARAM_BOOL, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** + * Refreshes the schema. + * This method cleans up all cached table schemas so that they can be re-created later + * to reflect the database schema change. + */ + public function refresh() + { + /** @var Cache $cache */ + $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; + if ($this->db->enableSchemaCache && $cache instanceof Cache) { + GroupDependency::invalidate($cache, $this->getCacheGroup()); + } + $this->_tableNames = []; + $this->_tables = []; + } + + /** + * Creates a query builder for the database. + * This method may be overridden by child classes to create a DBMS-specific query builder. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Returns all table names in the database. + * This method should be overridden by child classes in order to support this feature + * because the default implementation simply throws an exception. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + * @throws NotSupportedException if this method is called + */ + protected function findTableNames($schema = '') + { + throw new NotSupportedException(get_class($this) . ' does not support fetching all table names.'); + } + + /** + * Returns all unique indexes for the given table. + * Each array element is of the following structure: + * + * ~~~ + * [ + * 'IndexName1' => ['col1' [, ...]], + * 'IndexName2' => ['col2' [, ...]], + * ] + * ~~~ + * + * This method should be overridden by child classes in order to support this feature + * because the default implementation simply throws an exception + * @param TableSchema $table the table metadata + * @return array all unique indexes for the given table. + * @throws NotSupportedException if this method is called + */ + public function findUniqueIndexes($table) + { + throw new NotSupportedException(get_class($this) . ' does not support getting unique indexes information.'); + } + + /** + * Returns the ID of the last inserted row or sequence value. + * @param string $sequenceName name of the sequence object (required by some DBMS) + * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object + * @throws InvalidCallException if the DB connection is not active + * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php + */ + public function getLastInsertID($sequenceName = '') + { + if ($this->db->isActive) { + return $this->db->pdo->lastInsertId($sequenceName); + } else { + throw new InvalidCallException('DB Connection is not active.'); + } + } + + /** + * @return boolean whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint). + */ + public function supportsSavepoint() + { + return $this->db->enableSavepoint; + } + + /** + * Creates a new savepoint. + * @param string $name the savepoint name + */ + public function createSavepoint($name) + { + $this->db->createCommand("SAVEPOINT $name")->execute(); + } + + /** + * Releases an existing savepoint. + * @param string $name the savepoint name + */ + public function releaseSavepoint($name) + { + $this->db->createCommand("RELEASE SAVEPOINT $name")->execute(); + } + + /** + * Rolls back to a previously created savepoint. + * @param string $name the savepoint name + */ + public function rollBackSavepoint($name) + { + $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute(); + } + + /** + * Quotes a string value for use in a query. + * Note that if the parameter is not a string, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + if (!is_string($str)) { + return $str; + } + + $this->db->open(); + if (($value = $this->db->pdo->quote($str)) !== false) { + return $value; + } else { // the driver doesn't support quote (e.g. oci) + + return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; + } + } + + /** + * Quotes a table name for use in a query. + * If the table name contains schema prefix, the prefix will also be properly quoted. + * If the table name is already quoted or contains '(' or '{{', + * then this method will do nothing. + * @param string $name table name + * @return string the properly quoted table name + * @see quoteSimpleTableName() + */ + public function quoteTableName($name) + { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (strpos($name, '.') === false) { + return $this->quoteSimpleTableName($name); + } + $parts = explode('.', $name); + foreach ($parts as $i => $part) { + $parts[$i] = $this->quoteSimpleTableName($part); + } + + return implode('.', $parts); + + } + + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * If the column name is already quoted or contains '(', '[[' or '{{', + * then this method will do nothing. + * @param string $name column name + * @return string the properly quoted column name + * @see quoteSimpleColumnName() + */ + public function quoteColumnName($name) + { + if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (($pos = strrpos($name, '.')) !== false) { + $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.'; + $name = substr($name, $pos + 1); + } else { + $prefix = ''; + } + + return $prefix . $this->quoteSimpleColumnName($name); + } + + /** + * Quotes a simple table name for use in a query. + * A simple table name should contain the table name only without any schema prefix. + * If the table name is already quoted, this method will do nothing. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, "'") !== false ? $name : "'" . $name . "'"; + } + + /** + * Quotes a simple column name for use in a query. + * A simple column name should contain the column name only without any prefix. + * If the column name is already quoted or is the asterisk character '*', this method will do nothing. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; + } + + /** + * Returns the actual name of a given table name. + * This method will strip off curly brackets from the given table name + * and replace the percentage character '%' with [[Connection::tablePrefix]]. + * @param string $name the table name to be converted + * @return string the real name of the given table name + */ + public function getRawTableName($name) + { + if (strpos($name, '{{') !== false) { + $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); + + return str_replace('%', $this->db->tablePrefix, $name); + } else { + return $name; + } + } + + /** + * Extracts the PHP type from abstract DB type. + * @param ColumnSchema $column the column schema information + * @return string PHP type name + */ + protected function getColumnPhpType($column) + { + static $typeMap = [ // abstract type => php type + 'smallint' => 'integer', + 'integer' => 'integer', + 'boolean' => 'boolean', + 'float' => 'double', + ]; + if (isset($typeMap[$column->type])) { + if ($column->type === 'integer') { + return $column->unsigned ? 'string' : 'integer'; + } else { + return $typeMap[$column->type]; + } + } else { + return 'string'; + } + } } diff --git a/framework/db/StaleObjectException.php b/framework/db/StaleObjectException.php index efa8ee9083e..386dbcf7de7 100644 --- a/framework/db/StaleObjectException.php +++ b/framework/db/StaleObjectException.php @@ -13,11 +13,11 @@ */ class StaleObjectException extends Exception { - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Stale Object Exception'; - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return 'Stale Object Exception'; + } } diff --git a/framework/db/TableSchema.php b/framework/db/TableSchema.php index 83f431c1eff..86b925dc283 100644 --- a/framework/db/TableSchema.php +++ b/framework/db/TableSchema.php @@ -20,86 +20,85 @@ */ class TableSchema extends Object { - /** - * @var string the name of the schema that this table belongs to. - */ - public $schemaName; - /** - * @var string the name of this table. The schema name is not included. Use [[fullName]] to get the name with schema name prefix. - */ - public $name; - /** - * @var string the full name of this table, which includes the schema name prefix, if any. - * Note that if the schema name is the same as the [[Schema::defaultSchema|default schema name]], - * the schema name will not be included. - */ - public $fullName; - /** - * @var string[] primary keys of this table. - */ - public $primaryKey = []; - /** - * @var string sequence name for the primary key. Null if no sequence. - */ - public $sequenceName; - /** - * @var array foreign keys of this table. Each array element is of the following structure: - * - * ~~~ - * [ - * 'ForeignTableName', - * 'fk1' => 'pk1', // pk1 is in foreign table - * 'fk2' => 'pk2', // if composite foreign key - * ] - * ~~~ - */ - public $foreignKeys = []; - /** - * @var ColumnSchema[] column metadata of this table. Each array element is a [[ColumnSchema]] object, indexed by column names. - */ - public $columns = []; + /** + * @var string the name of the schema that this table belongs to. + */ + public $schemaName; + /** + * @var string the name of this table. The schema name is not included. Use [[fullName]] to get the name with schema name prefix. + */ + public $name; + /** + * @var string the full name of this table, which includes the schema name prefix, if any. + * Note that if the schema name is the same as the [[Schema::defaultSchema|default schema name]], + * the schema name will not be included. + */ + public $fullName; + /** + * @var string[] primary keys of this table. + */ + public $primaryKey = []; + /** + * @var string sequence name for the primary key. Null if no sequence. + */ + public $sequenceName; + /** + * @var array foreign keys of this table. Each array element is of the following structure: + * + * ~~~ + * [ + * 'ForeignTableName', + * 'fk1' => 'pk1', // pk1 is in foreign table + * 'fk2' => 'pk2', // if composite foreign key + * ] + * ~~~ + */ + public $foreignKeys = []; + /** + * @var ColumnSchema[] column metadata of this table. Each array element is a [[ColumnSchema]] object, indexed by column names. + */ + public $columns = []; + /** + * Gets the named column metadata. + * This is a convenient method for retrieving a named column even if it does not exist. + * @param string $name column name + * @return ColumnSchema metadata of the named column. Null if the named column does not exist. + */ + public function getColumn($name) + { + return isset($this->columns[$name]) ? $this->columns[$name] : null; + } - /** - * Gets the named column metadata. - * This is a convenient method for retrieving a named column even if it does not exist. - * @param string $name column name - * @return ColumnSchema metadata of the named column. Null if the named column does not exist. - */ - public function getColumn($name) - { - return isset($this->columns[$name]) ? $this->columns[$name] : null; - } + /** + * Returns the names of all columns in this table. + * @return array list of column names + */ + public function getColumnNames() + { + return array_keys($this->columns); + } - /** - * Returns the names of all columns in this table. - * @return array list of column names - */ - public function getColumnNames() - { - return array_keys($this->columns); - } - - /** - * Manually specifies the primary key for this table. - * @param string|array $keys the primary key (can be composite) - * @throws InvalidParamException if the specified key cannot be found in the table. - */ - public function fixPrimaryKey($keys) - { - if (!is_array($keys)) { - $keys = [$keys]; - } - $this->primaryKey = $keys; - foreach ($this->columns as $column) { - $column->isPrimaryKey = false; - } - foreach ($keys as $key) { - if (isset($this->columns[$key])) { - $this->columns[$key]->isPrimaryKey = true; - } else { - throw new InvalidParamException("Primary key '$key' cannot be found in table '{$this->name}'."); - } - } - } + /** + * Manually specifies the primary key for this table. + * @param string|array $keys the primary key (can be composite) + * @throws InvalidParamException if the specified key cannot be found in the table. + */ + public function fixPrimaryKey($keys) + { + if (!is_array($keys)) { + $keys = [$keys]; + } + $this->primaryKey = $keys; + foreach ($this->columns as $column) { + $column->isPrimaryKey = false; + } + foreach ($keys as $key) { + if (isset($this->columns[$key])) { + $this->columns[$key]->isPrimaryKey = true; + } else { + throw new InvalidParamException("Primary key '$key' cannot be found in table '{$this->name}'."); + } + } + } } diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index eefe7092925..d40c7d691ec 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -25,7 +25,7 @@ * $connection->createCommand($sql2)->execute(); * //.... other SQL executions * $transaction->commit(); - * } catch(Exception $e) { + * } catch (Exception $e) { * $transaction->rollBack(); * } * ~~~ @@ -38,104 +38,107 @@ */ class Transaction extends \yii\base\Object { - /** - * @var Connection the database connection that this transaction is associated with. - */ - public $db; - /** - * @var integer the nesting level of the transaction. 0 means the outermost level. - */ - private $_level = 0; - - /** - * Returns a value indicating whether this transaction is active. - * @return boolean whether this transaction is active. Only an active transaction - * can [[commit()]] or [[rollBack()]]. - */ - public function getIsActive() - { - return $this->_level > 0 && $this->db && $this->db->isActive; - } - - /** - * Begins a transaction. - * @throws InvalidConfigException if [[db]] is `null`. - */ - public function begin() - { - if ($this->db === null) { - throw new InvalidConfigException('Transaction::db must be set.'); - } - $this->db->open(); - - if ($this->_level == 0) { - Yii::trace('Begin transaction', __METHOD__); - $this->db->pdo->beginTransaction(); - $this->_level = 1; - return; - } - - $schema = $this->db->getSchema(); - if ($schema->supportsSavepoint()) { - Yii::trace('Set savepoint ' . $this->_level, __METHOD__); - $schema->createSavepoint('LEVEL' . $this->_level); - } else { - Yii::info('Transaction not started: nested transaction not supported', __METHOD__); - } - $this->_level++; - } - - /** - * Commits a transaction. - * @throws Exception if the transaction is not active - */ - public function commit() - { - if (!$this->getIsActive()) { - throw new Exception('Failed to commit transaction: transaction was inactive.'); - } - - $this->_level--; - if ($this->_level == 0) { - Yii::trace('Commit transaction', __METHOD__); - $this->db->pdo->commit(); - return; - } - - $schema = $this->db->getSchema(); - if ($schema->supportsSavepoint()) { - Yii::trace('Release savepoint ' . $this->_level, __METHOD__); - $schema->releaseSavepoint('LEVEL' . $this->_level); - } else { - Yii::info('Transaction not committed: nested transaction not supported', __METHOD__); - } - } - - /** - * Rolls back a transaction. - * @throws Exception if the transaction is not active - */ - public function rollBack() - { - if (!$this->getIsActive()) { - throw new Exception('Failed to roll back transaction: transaction was inactive.'); - } - - $this->_level--; - if ($this->_level == 0) { - Yii::trace('Roll back transaction', __METHOD__); - $this->db->pdo->rollBack(); - return; - } - - $schema = $this->db->getSchema(); - if ($schema->supportsSavepoint()) { - Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__); - $schema->rollBackSavepoint('LEVEL' . $this->_level); - } else { - Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__); - // throw an exception to fail the outer transaction - throw new Exception('Roll back failed: nested transaction not supported.'); - } - } + /** + * @var Connection the database connection that this transaction is associated with. + */ + public $db; + /** + * @var integer the nesting level of the transaction. 0 means the outermost level. + */ + private $_level = 0; + + /** + * Returns a value indicating whether this transaction is active. + * @return boolean whether this transaction is active. Only an active transaction + * can [[commit()]] or [[rollBack()]]. + */ + public function getIsActive() + { + return $this->_level > 0 && $this->db && $this->db->isActive; + } + + /** + * Begins a transaction. + * @throws InvalidConfigException if [[db]] is `null`. + */ + public function begin() + { + if ($this->db === null) { + throw new InvalidConfigException('Transaction::db must be set.'); + } + $this->db->open(); + + if ($this->_level == 0) { + Yii::trace('Begin transaction', __METHOD__); + $this->db->pdo->beginTransaction(); + $this->_level = 1; + + return; + } + + $schema = $this->db->getSchema(); + if ($schema->supportsSavepoint()) { + Yii::trace('Set savepoint ' . $this->_level, __METHOD__); + $schema->createSavepoint('LEVEL' . $this->_level); + } else { + Yii::info('Transaction not started: nested transaction not supported', __METHOD__); + } + $this->_level++; + } + + /** + * Commits a transaction. + * @throws Exception if the transaction is not active + */ + public function commit() + { + if (!$this->getIsActive()) { + throw new Exception('Failed to commit transaction: transaction was inactive.'); + } + + $this->_level--; + if ($this->_level == 0) { + Yii::trace('Commit transaction', __METHOD__); + $this->db->pdo->commit(); + + return; + } + + $schema = $this->db->getSchema(); + if ($schema->supportsSavepoint()) { + Yii::trace('Release savepoint ' . $this->_level, __METHOD__); + $schema->releaseSavepoint('LEVEL' . $this->_level); + } else { + Yii::info('Transaction not committed: nested transaction not supported', __METHOD__); + } + } + + /** + * Rolls back a transaction. + * @throws Exception if the transaction is not active + */ + public function rollBack() + { + if (!$this->getIsActive()) { + throw new Exception('Failed to roll back transaction: transaction was inactive.'); + } + + $this->_level--; + if ($this->_level == 0) { + Yii::trace('Roll back transaction', __METHOD__); + $this->db->pdo->rollBack(); + + return; + } + + $schema = $this->db->getSchema(); + if ($schema->supportsSavepoint()) { + Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__); + $schema->rollBackSavepoint('LEVEL' . $this->_level); + } else { + Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__); + // throw an exception to fail the outer transaction + throw new Exception('Roll back failed: nested transaction not supported.'); + } + } } diff --git a/framework/db/cubrid/QueryBuilder.php b/framework/db/cubrid/QueryBuilder.php index 399599ec90e..24603faba13 100644 --- a/framework/db/cubrid/QueryBuilder.php +++ b/framework/db/cubrid/QueryBuilder.php @@ -17,74 +17,76 @@ */ class QueryBuilder extends \yii\db\QueryBuilder { - /** - * @var array mapping from abstract column types (keys) to physical column types (values). - */ - public $typeMap = [ - Schema::TYPE_PK => 'int NOT NULL AUTO_INCREMENT PRIMARY KEY', - Schema::TYPE_BIGPK => 'bigint NOT NULL AUTO_INCREMENT PRIMARY KEY', - Schema::TYPE_STRING => 'varchar(255)', - Schema::TYPE_TEXT => 'varchar', - Schema::TYPE_SMALLINT => 'smallint', - Schema::TYPE_INTEGER => 'int', - Schema::TYPE_BIGINT => 'bigint', - Schema::TYPE_FLOAT => 'float(7)', - Schema::TYPE_DECIMAL => 'decimal(10,0)', - Schema::TYPE_DATETIME => 'datetime', - Schema::TYPE_TIMESTAMP => 'timestamp', - Schema::TYPE_TIME => 'time', - Schema::TYPE_DATE => 'date', - Schema::TYPE_BINARY => 'blob', - Schema::TYPE_BOOLEAN => 'smallint', - Schema::TYPE_MONEY => 'decimal(19,4)', - ]; + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'int NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'varchar', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'int', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'float(7)', + Schema::TYPE_DECIMAL => 'decimal(10,0)', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'blob', + Schema::TYPE_BOOLEAN => 'smallint', + Schema::TYPE_MONEY => 'decimal(19,4)', + ]; - /** - * Creates a SQL statement for resetting the sequence value of a table's primary key. - * The sequence will be reset such that the primary key of the next new row inserted - * will have the specified value or 1. - * @param string $tableName the name of the table whose primary key sequence will be reset - * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, - * the next new row's primary key will have a value 1. - * @return string the SQL statement for resetting sequence - * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. - */ - public function resetSequence($tableName, $value = null) - { - $table = $this->db->getTableSchema($tableName); - if ($table !== null && $table->sequenceName !== null) { - $tableName = $this->db->quoteTableName($tableName); - if ($value === null) { - $key = reset($table->primaryKey); - $value = (int)$this->db->createCommand("SELECT MAX(`$key`) FROM " . $this->db->schema->quoteTableName($tableName))->queryScalar() + 1; - } else { - $value = (int)$value; - } - return "ALTER TABLE " . $this->db->schema->quoteTableName($tableName) . " AUTO_INCREMENT=$value;"; - } elseif ($table === null) { - throw new InvalidParamException("Table not found: $tableName"); - } else { - throw new InvalidParamException("There is not sequence associated with table '$tableName'."); - } - } + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $tableName the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return string the SQL statement for resetting sequence + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function resetSequence($tableName, $value = null) + { + $table = $this->db->getTableSchema($tableName); + if ($table !== null && $table->sequenceName !== null) { + $tableName = $this->db->quoteTableName($tableName); + if ($value === null) { + $key = reset($table->primaryKey); + $value = (int) $this->db->createCommand("SELECT MAX(`$key`) FROM " . $this->db->schema->quoteTableName($tableName))->queryScalar() + 1; + } else { + $value = (int) $value; + } - /** - * @inheritdoc - */ - public function buildLimit($limit, $offset) - { - $sql = ''; - // limit is not optional in CUBRID - // http://www.cubrid.org/manual/90/en/LIMIT%20Clause - // "You can specify a very big integer for row_count to display to the last row, starting from a specific row." - if ($this->hasLimit($limit)) { - $sql = 'LIMIT ' . $limit; - if ($this->hasOffset($offset)) { - $sql .= ' OFFSET ' . $offset; - } - } elseif ($this->hasOffset($offset)) { - $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1 - } - return $sql; - } + return "ALTER TABLE " . $this->db->schema->quoteTableName($tableName) . " AUTO_INCREMENT=$value;"; + } elseif ($table === null) { + throw new InvalidParamException("Table not found: $tableName"); + } else { + throw new InvalidParamException("There is not sequence associated with table '$tableName'."); + } + } + + /** + * @inheritdoc + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + // limit is not optional in CUBRID + // http://www.cubrid.org/manual/90/en/LIMIT%20Clause + // "You can specify a very big integer for row_count to display to the last row, starting from a specific row." + if ($this->hasLimit($limit)) { + $sql = 'LIMIT ' . $limit; + if ($this->hasOffset($offset)) { + $sql .= ' OFFSET ' . $offset; + } + } elseif ($this->hasOffset($offset)) { + $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1 + } + + return $sql; + } } diff --git a/framework/db/cubrid/Schema.php b/framework/db/cubrid/Schema.php index 5c31997b46d..816882372ed 100644 --- a/framework/db/cubrid/Schema.php +++ b/framework/db/cubrid/Schema.php @@ -19,256 +19,257 @@ */ class Schema extends \yii\db\Schema { - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - * Please refer to [CUBRID manual](http://www.cubrid.org/manual/91/en/sql/datatype.html) for - * details on data types. - */ - public $typeMap = [ - // Numeric data types - 'short' => self::TYPE_SMALLINT, - 'smallint' => self::TYPE_SMALLINT, - 'int' => self::TYPE_INTEGER, - 'integer' => self::TYPE_INTEGER, - 'bigint' => self::TYPE_BIGINT, - 'numeric' => self::TYPE_DECIMAL, - 'decimal' => self::TYPE_DECIMAL, - 'float' => self::TYPE_FLOAT, - 'real' => self::TYPE_FLOAT, - 'double' => self::TYPE_FLOAT, - 'double precision' => self::TYPE_FLOAT, - 'monetary' => self::TYPE_MONEY, - // Date/Time data types - 'date' => self::TYPE_DATE, - 'time' => self::TYPE_TIME, - 'timestamp' => self::TYPE_TIMESTAMP, - 'datetime' => self::TYPE_DATETIME, - // String data types - 'char' => self::TYPE_STRING, - 'varchar' => self::TYPE_STRING, - 'char varying' => self::TYPE_STRING, - 'nchar' => self::TYPE_STRING, - 'nchar varying' => self::TYPE_STRING, - 'string' => self::TYPE_STRING, - // BLOB/CLOB data types - 'blob' => self::TYPE_BINARY, - 'clob' => self::TYPE_BINARY, - // Bit string data types - 'bit' => self::TYPE_STRING, - 'bit varying' => self::TYPE_STRING, - // Collection data types (considered strings for now) - 'set' => self::TYPE_STRING, - 'multiset' => self::TYPE_STRING, - 'list' => self::TYPE_STRING, - 'sequence' => self::TYPE_STRING, - 'enum' => self::TYPE_STRING, - ]; + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + * Please refer to [CUBRID manual](http://www.cubrid.org/manual/91/en/sql/datatype.html) for + * details on data types. + */ + public $typeMap = [ + // Numeric data types + 'short' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'int' => self::TYPE_INTEGER, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'numeric' => self::TYPE_DECIMAL, + 'decimal' => self::TYPE_DECIMAL, + 'float' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + 'double' => self::TYPE_FLOAT, + 'double precision' => self::TYPE_FLOAT, + 'monetary' => self::TYPE_MONEY, + // Date/Time data types + 'date' => self::TYPE_DATE, + 'time' => self::TYPE_TIME, + 'timestamp' => self::TYPE_TIMESTAMP, + 'datetime' => self::TYPE_DATETIME, + // String data types + 'char' => self::TYPE_STRING, + 'varchar' => self::TYPE_STRING, + 'char varying' => self::TYPE_STRING, + 'nchar' => self::TYPE_STRING, + 'nchar varying' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + // BLOB/CLOB data types + 'blob' => self::TYPE_BINARY, + 'clob' => self::TYPE_BINARY, + // Bit string data types + 'bit' => self::TYPE_STRING, + 'bit varying' => self::TYPE_STRING, + // Collection data types (considered strings for now) + 'set' => self::TYPE_STRING, + 'multiset' => self::TYPE_STRING, + 'list' => self::TYPE_STRING, + 'sequence' => self::TYPE_STRING, + 'enum' => self::TYPE_STRING, + ]; + /** + * @inheritdoc + */ + public function releaseSavepoint($name) + { + // does nothing as cubrid does not support this + } - /** - * @inheritdoc - */ - public function releaseSavepoint($name) - { - // does nothing as cubrid does not support this - } + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } - /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteSimpleTableName($name) - { - return strpos($name, '"') !== false ? $name : '"' . $name . '"'; - } + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; + } - /** - * Quotes a column name for use in a query. - * A simple column name has no prefix. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; - } + /** + * Quotes a string value for use in a query. + * Note that if the parameter is not a string, it will be returned without change. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + if (!is_string($str)) { + return $str; + } - /** - * Quotes a string value for use in a query. - * Note that if the parameter is not a string, it will be returned without change. - * @param string $str string to be quoted - * @return string the properly quoted string - * @see http://www.php.net/manual/en/function.PDO-quote.php - */ - public function quoteValue($str) - { - if (!is_string($str)) { - return $str; - } + $this->db->open(); + // workaround for broken PDO::quote() implementation in CUBRID 9.1.0 http://jira.cubrid.org/browse/APIS-658 + $version = $this->db->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION); + if (version_compare($version, '8.4.4.0002', '<') || $version[0] == '9' && version_compare($version, '9.2.0.0002', '<=')) { + return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; + } else { + return $this->db->pdo->quote($str); + } + } - $this->db->open(); - // workaround for broken PDO::quote() implementation in CUBRID 9.1.0 http://jira.cubrid.org/browse/APIS-658 - $version = $this->db->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION); - if (version_compare($version, '8.4.4.0002', '<') || $version[0] == '9' && version_compare($version, '9.2.0.0002', '<=')) { - return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; - } else { - return $this->db->pdo->quote($str); - } - } + /** + * Creates a query builder for the CUBRID database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } - /** - * Creates a query builder for the CUBRID database. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTableSchema($name) + { + $this->db->open(); + $tableInfo = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE, $name); - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function loadTableSchema($name) - { - $this->db->open(); - $tableInfo = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE, $name); + if (!isset($tableInfo[0]['NAME'])) { + return null; + } - if (!isset($tableInfo[0]['NAME'])) { - return null; - } + $table = new TableSchema(); + $table->fullName = $table->name = $tableInfo[0]['NAME']; - $table = new TableSchema(); - $table->fullName = $table->name = $tableInfo[0]['NAME']; + $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteSimpleTableName($table->name); + $columns = $this->db->createCommand($sql)->queryAll(); - $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteSimpleTableName($table->name); - $columns = $this->db->createCommand($sql)->queryAll(); + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); + $table->columns[$column->name] = $column; + } - foreach ($columns as $info) { - $column = $this->loadColumnSchema($info); - $table->columns[$column->name] = $column; - } + $primaryKeys = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_PRIMARY_KEY, $table->name); + foreach ($primaryKeys as $key) { + $column = $table->columns[$key['ATTR_NAME']]; + $column->isPrimaryKey = true; + $table->primaryKey[] = $column->name; + if ($column->autoIncrement) { + $table->sequenceName = ''; + } + } - $primaryKeys = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_PRIMARY_KEY, $table->name); - foreach ($primaryKeys as $key) { - $column = $table->columns[$key['ATTR_NAME']]; - $column->isPrimaryKey = true; - $table->primaryKey[] = $column->name; - if ($column->autoIncrement) { - $table->sequenceName = ''; - } - } + $foreignKeys = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $table->name); + foreach ($foreignKeys as $key) { + if (isset($table->foreignKeys[$key['FK_NAME']])) { + $table->foreignKeys[$key['FK_NAME']][$key['FKCOLUMN_NAME']] = $key['PKCOLUMN_NAME']; + } else { + $table->foreignKeys[$key['FK_NAME']] = [ + $key['PKTABLE_NAME'], + $key['FKCOLUMN_NAME'] => $key['PKCOLUMN_NAME'] + ]; + } + } + $table->foreignKeys = array_values($table->foreignKeys); - $foreignKeys = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $table->name); - foreach ($foreignKeys as $key) { - if (isset($table->foreignKeys[$key['FK_NAME']])) { - $table->foreignKeys[$key['FK_NAME']][$key['FKCOLUMN_NAME']] = $key['PKCOLUMN_NAME']; - } else { - $table->foreignKeys[$key['FK_NAME']] = [ - $key['PKTABLE_NAME'], - $key['FKCOLUMN_NAME'] => $key['PKCOLUMN_NAME'] - ]; - } - } - $table->foreignKeys = array_values($table->foreignKeys); + return $table; + } - return $table; - } + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema(); + $column->name = $info['Field']; + $column->allowNull = $info['Null'] === 'YES'; + $column->isPrimaryKey = false; // primary key will be set by loadTableSchema() later + $column->autoIncrement = stripos($info['Extra'], 'auto_increment') !== false; - $column->name = $info['Field']; - $column->allowNull = $info['Null'] === 'YES'; - $column->isPrimaryKey = false; // primary key will be set by loadTableSchema() later - $column->autoIncrement = stripos($info['Extra'], 'auto_increment') !== false; + $column->dbType = strtolower($info['Type']); + $column->unsigned = strpos($column->dbType, 'unsigned') !== false; - $column->dbType = strtolower($info['Type']); - $column->unsigned = strpos($column->dbType, 'unsigned') !== false; + $column->type = self::TYPE_STRING; + if (preg_match('/^([\w ]+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + if ($type === 'enum') { + $values = explode(',', $matches[2]); + foreach ($values as $i => $value) { + $values[$i] = trim($value, "'"); + } + $column->enumValues = $values; + } else { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int) $values[0]; + if (isset($values[1])) { + $column->scale = (int) $values[1]; + } + } + } + } - $column->type = self::TYPE_STRING; - if (preg_match('/^([\w ]+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { - $type = $matches[1]; - if (isset($this->typeMap[$type])) { - $column->type = $this->typeMap[$type]; - } - if (!empty($matches[2])) { - if ($type === 'enum') { - $values = explode(',', $matches[2]); - foreach ($values as $i => $value) { - $values[$i] = trim($value, "'"); - } - $column->enumValues = $values; - } else { - $values = explode(',', $matches[2]); - $column->size = $column->precision = (int)$values[0]; - if (isset($values[1])) { - $column->scale = (int)$values[1]; - } - } - } - } + $column->phpType = $this->getColumnPhpType($column); - $column->phpType = $this->getColumnPhpType($column); + if ($column->type === 'timestamp' && $info['Default'] === 'CURRENT_TIMESTAMP' || + $column->type === 'datetime' && $info['Default'] === 'SYS_DATETIME' || + $column->type === 'date' && $info['Default'] === 'SYS_DATE' || + $column->type === 'time' && $info['Default'] === 'SYS_TIME' + ) { + $column->defaultValue = new Expression($info['Default']); + } else { + $column->defaultValue = $column->typecast($info['Default']); + } - if ($column->type === 'timestamp' && $info['Default'] === 'CURRENT_TIMESTAMP' || - $column->type === 'datetime' && $info['Default'] === 'SYS_DATETIME' || - $column->type === 'date' && $info['Default'] === 'SYS_DATE' || - $column->type === 'time' && $info['Default'] === 'SYS_TIME' - ) { - $column->defaultValue = new Expression($info['Default']); - } else { - $column->defaultValue = $column->typecast($info['Default']); - } + return $column; + } - return $column; - } + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + $this->db->open(); + $tables = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE); + $tableNames = []; + foreach ($tables as $table) { + // do not list system tables + if ($table['TYPE'] != 0) { + $tableNames[] = $table['NAME']; + } + } - /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - $this->db->open(); - $tables = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE); - $tableNames = []; - foreach ($tables as $table) { - // do not list system tables - if ($table['TYPE'] != 0) { - $tableNames[] = $table['NAME']; - } - } - return $tableNames; - } + return $tableNames; + } - /** - * Determines the PDO type for the given PHP data value. - * @param mixed $data the data whose PDO type is to be determined - * @return integer the PDO type - * @see http://www.php.net/manual/en/pdo.constants.php - */ - public function getPdoType($data) - { - static $typeMap = [ - // php type => PDO type - 'boolean' => \PDO::PARAM_INT, // PARAM_BOOL is not supported by CUBRID PDO - 'integer' => \PDO::PARAM_INT, - 'string' => \PDO::PARAM_STR, - 'resource' => \PDO::PARAM_LOB, - 'NULL' => \PDO::PARAM_NULL, - ]; - $type = gettype($data); - return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; - } + /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + static $typeMap = [ + // php type => PDO type + 'boolean' => \PDO::PARAM_INT, // PARAM_BOOL is not supported by CUBRID PDO + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } } diff --git a/framework/db/mssql/PDO.php b/framework/db/mssql/PDO.php index 59c2455ddc0..dc6bb3dbbe7 100644 --- a/framework/db/mssql/PDO.php +++ b/framework/db/mssql/PDO.php @@ -16,46 +16,49 @@ */ class PDO extends \PDO { - /** - * Returns value of the last inserted ID. - * @param string|null $sequence the sequence name. Defaults to null. - * @return integer last inserted ID value. - */ - public function lastInsertId($sequence = null) - { - return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); - } - - /** - * Starts a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not - * natively support transactions. - * @return boolean the result of a transaction start. - */ - public function beginTransaction() - { - $this->exec('BEGIN TRANSACTION'); - return true; - } - - /** - * Commits a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not - * natively support transactions. - * @return boolean the result of a transaction commit. - */ - public function commit() - { - $this->exec('COMMIT TRANSACTION'); - return true; - } - - /** - * Rollbacks a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not - * natively support transactions. - * @return boolean the result of a transaction roll back. - */ - public function rollBack() - { - $this->exec('ROLLBACK TRANSACTION'); - return true; - } + /** + * Returns value of the last inserted ID. + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); + } + + /** + * Starts a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction start. + */ + public function beginTransaction() + { + $this->exec('BEGIN TRANSACTION'); + + return true; + } + + /** + * Commits a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction commit. + */ + public function commit() + { + $this->exec('COMMIT TRANSACTION'); + + return true; + } + + /** + * Rollbacks a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction roll back. + */ + public function rollBack() + { + $this->exec('ROLLBACK TRANSACTION'); + + return true; + } } diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php index 2eb56a6a6a2..10c7ca4f1df 100644 --- a/framework/db/mssql/QueryBuilder.php +++ b/framework/db/mssql/QueryBuilder.php @@ -17,27 +17,27 @@ */ class QueryBuilder extends \yii\db\QueryBuilder { - /** - * @var array mapping from abstract column types (keys) to physical column types (values). - */ - public $typeMap = [ - Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY', - Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY', - Schema::TYPE_STRING => 'varchar(255)', - Schema::TYPE_TEXT => 'text', - Schema::TYPE_SMALLINT => 'smallint', - Schema::TYPE_INTEGER => 'int', - Schema::TYPE_BIGINT => 'bigint', - Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', - Schema::TYPE_DATETIME => 'datetime', - Schema::TYPE_TIMESTAMP => 'timestamp', - Schema::TYPE_TIME => 'time', - Schema::TYPE_DATE => 'date', - Schema::TYPE_BINARY => 'binary', - Schema::TYPE_BOOLEAN => 'bit', - Schema::TYPE_MONEY => 'decimal(19,4)', - ]; + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'int', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'binary', + Schema::TYPE_BOOLEAN => 'bit', + Schema::TYPE_MONEY => 'decimal(19,4)', + ]; // public function update($table, $columns, $condition, &$params) // { @@ -49,91 +49,94 @@ class QueryBuilder extends \yii\db\QueryBuilder // return ''; // } - /** - * @param integer $limit - * @param integer $offset - * @return string the LIMIT and OFFSET clauses built from [[\yii\db\Query::$limit]]. - */ - public function buildLimit($limit, $offset = 0) - { - $hasOffset = $this->hasOffset($offset); - $hasLimit = $this->hasLimit($limit); - if ($hasOffset || $hasLimit) { - // http://technet.microsoft.com/en-us/library/gg699618.aspx - $sql = 'OFFSET ' . ($hasOffset ? $offset : '0'); - if ($hasLimit) { - $sql .= " FETCH NEXT $limit ROWS ONLY"; - } - return $sql; - } else { - return ''; - } - } + /** + * @param integer $limit + * @param integer $offset + * @return string the LIMIT and OFFSET clauses built from [[\yii\db\Query::$limit]]. + */ + public function buildLimit($limit, $offset = 0) + { + $hasOffset = $this->hasOffset($offset); + $hasLimit = $this->hasLimit($limit); + if ($hasOffset || $hasLimit) { + // http://technet.microsoft.com/en-us/library/gg699618.aspx + $sql = 'OFFSET ' . ($hasOffset ? $offset : '0'); + if ($hasLimit) { + $sql .= " FETCH NEXT $limit ROWS ONLY"; + } + + return $sql; + } else { + return ''; + } + } // public function resetSequence($table, $value = null) // { // return ''; // } - /** - * Builds a SQL statement for renaming a DB table. - * @param string $table the table to be renamed. The name will be properly quoted by the method. - * @param string $newName the new table name. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB table. - */ - public function renameTable($table, $newName) - { - return "sp_rename '$table', '$newName'"; - } + /** + * Builds a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($table, $newName) + { + return "sp_rename '$table', '$newName'"; + } + + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + */ + public function renameColumn($table, $name, $newName) + { + return "sp_rename '$table.$name', '$newName', 'COLUMN'"; + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + $type = $this->getColumnType($type); + $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' + . $this->db->quoteColumnName($column) . ' ' + . $this->getColumnType($type); - /** - * Builds a SQL statement for renaming a column. - * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. - * @param string $name the old name of the column. The name will be properly quoted by the method. - * @param string $newName the new name of the column. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB column. - */ - public function renameColumn($table, $name, $newName) - { - return "sp_rename '$table.$name', '$newName', 'COLUMN'"; - } + return $sql; + } - /** - * Builds a SQL statement for changing the definition of a column. - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) - * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. - * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. - * @return string the SQL statement for changing the definition of a column. - */ - public function alterColumn($table, $column, $type) - { - $type = $this->getColumnType($type); - $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' - . $this->db->quoteColumnName($column) . ' ' - . $this->getColumnType($type); - return $sql; - } + /** + * Builds a SQL statement for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. + * @return string the SQL statement for checking integrity + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + if ($schema !== '') { + $table = "{$schema}.{$table}"; + } + $table = $this->db->quoteTableName($table); + if ($this->db->getTableSchema($table) === null) { + throw new InvalidParamException("Table not found: $table"); + } + $enable = $check ? 'CHECK' : 'NOCHECK'; - /** - * Builds a SQL statement for enabling or disabling integrity check. - * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. - * @return string the SQL statement for checking integrity - * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - if ($schema !== '') { - $table = "{$schema}.{$table}"; - } - $table = $this->db->quoteTableName($table); - if ($this->db->getTableSchema($table) === null) { - throw new InvalidParamException("Table not found: $table"); - } - $enable = $check ? 'CHECK' : 'NOCHECK'; - return "ALTER TABLE {$table} {$enable} CONSTRAINT ALL"; - } + return "ALTER TABLE {$table} {$enable} CONSTRAINT ALL"; + } } diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 4b3b1da5911..d73b918be66 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -17,362 +17,364 @@ */ class Schema extends \yii\db\Schema { - /** - * @var string the default schema used for the current session. - */ - public $defaultSchema = 'dbo'; - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - */ - public $typeMap = [ - // exact numbers - 'bigint' => self::TYPE_BIGINT, - 'numeric' => self::TYPE_DECIMAL, - 'bit' => self::TYPE_SMALLINT, - 'smallint' => self::TYPE_SMALLINT, - 'decimal' => self::TYPE_DECIMAL, - 'smallmoney' => self::TYPE_MONEY, - 'int' => self::TYPE_INTEGER, - 'tinyint' => self::TYPE_SMALLINT, - 'money' => self::TYPE_MONEY, - - // approximate numbers - 'float' => self::TYPE_FLOAT, - 'real' => self::TYPE_FLOAT, - - // date and time - 'date' => self::TYPE_DATE, - 'datetimeoffset' => self::TYPE_DATETIME, - 'datetime2' => self::TYPE_DATETIME, - 'smalldatetime' => self::TYPE_DATETIME, - 'datetime' => self::TYPE_DATETIME, - 'time' => self::TYPE_TIME, - - // character strings - 'char' => self::TYPE_STRING, - 'varchar' => self::TYPE_STRING, - 'text' => self::TYPE_TEXT, - - // unicode character strings - 'nchar' => self::TYPE_STRING, - 'nvarchar' => self::TYPE_STRING, - 'ntext' => self::TYPE_TEXT, - - // binary strings - 'binary' => self::TYPE_BINARY, - 'varbinary' => self::TYPE_BINARY, - 'image' => self::TYPE_BINARY, - - // other data types - // 'cursor' type cannot be used with tables - 'timestamp' => self::TYPE_TIMESTAMP, - 'hierarchyid' => self::TYPE_STRING, - 'uniqueidentifier' => self::TYPE_STRING, - 'sql_variant' => self::TYPE_STRING, - 'xml' => self::TYPE_STRING, - 'table' => self::TYPE_STRING, - ]; - - /** - * @inheritdoc - */ - public function createSavepoint($name) - { - $this->db->createCommand("SAVE TRANSACTION $name")->execute(); - } - - /** - * @inheritdoc - */ - public function releaseSavepoint($name) - { - // does nothing as MSSQL does not support this - } - - /** - * @inheritdoc - */ - public function rollBackSavepoint($name) - { - $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute(); - } - - /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name. - * @return string the properly quoted table name. - */ - public function quoteSimpleTableName($name) - { - return strpos($name, '[') === false ? "[{$name}]" : $name; - } - - /** - * Quotes a column name for use in a query. - * A simple column name has no prefix. - * @param string $name column name. - * @return string the properly quoted column name. - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '[') === false && $name !== '*' ? "[{$name}]" : $name; - } - - /** - * Creates a query builder for the MSSQL database. - * @return QueryBuilder query builder interface. - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. - */ - public function loadTableSchema($name) - { - $table = new TableSchema(); - $this->resolveTableNames($table, $name); - $this->findPrimaryKeys($table); - if ($this->findColumns($table)) { - $this->findForeignKeys($table); - return $table; - } else { - return null; - } - } - - /** - * Resolves the table name and schema name (if any). - * @param TableSchema $table the table metadata object - * @param string $name the table name - */ - protected function resolveTableNames($table, $name) - { - $parts = explode('.', str_replace(['[', ']'], '', $name)); - $partCount = count($parts); - if ($partCount == 3) { - // catalog name, schema name and table name passed - $table->catalogName = $parts[0]; - $table->schemaName = $parts[1]; - $table->name = $parts[2]; - $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name; - } elseif ($partCount == 2) { - // only schema name and table name passed - $table->schemaName = $parts[0]; - $table->name = $parts[1]; - $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; - } else { - // only table name passed - $table->schemaName = $this->defaultSchema; - $table->fullName = $table->name = $parts[0]; - } - } - - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema(); - - $column->name = $info['column_name']; - $column->allowNull = $info['is_nullable'] == 'YES'; - $column->dbType = $info['data_type']; - $column->enumValues = []; // mssql has only vague equivalents to enum - $column->isPrimaryKey = null; // primary key will be determined in findColumns() method - $column->autoIncrement = $info['is_identity'] == 1; - $column->unsigned = stripos($column->dbType, 'unsigned') !== false; - $column->comment = $info['comment'] === null ? '' : $info['comment']; - - $column->type = self::TYPE_STRING; - if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { - $type = $matches[1]; - if (isset($this->typeMap[$type])) { - $column->type = $this->typeMap[$type]; - } - if (!empty($matches[2])) { - $values = explode(',', $matches[2]); - $column->size = $column->precision = (int)$values[0]; - if (isset($values[1])) { - $column->scale = (int)$values[1]; - } - if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { - $column->type = 'boolean'; - } elseif ($type === 'bit') { - if ($column->size > 32) { - $column->type = 'bigint'; - } elseif ($column->size === 32) { - $column->type = 'integer'; - } - } - } - } - - $column->phpType = $this->getColumnPhpType($column); - - if ($info['column_default'] == '(NULL)') { - $info['column_default'] = null; - } - if ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP') { - $column->defaultValue = $column->typecast($info['column_default']); - } - - return $column; - } - - /** - * Collects the metadata of table columns. - * @param TableSchema $table the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - $columnsTableName = 'information_schema.columns'; - $whereSql = "[t1].[table_name] = '{$table->name}'"; - if ($table->catalogName !== null) { - $columnsTableName = "{$table->catalogName}.{$columnsTableName}"; - $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'"; - } - if ($table->schemaName !== null) { - $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'"; - } - $columnsTableName = $this->quoteTableName($columnsTableName); - - $sql = << self::TYPE_BIGINT, + 'numeric' => self::TYPE_DECIMAL, + 'bit' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'decimal' => self::TYPE_DECIMAL, + 'smallmoney' => self::TYPE_MONEY, + 'int' => self::TYPE_INTEGER, + 'tinyint' => self::TYPE_SMALLINT, + 'money' => self::TYPE_MONEY, + + // approximate numbers + 'float' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + + // date and time + 'date' => self::TYPE_DATE, + 'datetimeoffset' => self::TYPE_DATETIME, + 'datetime2' => self::TYPE_DATETIME, + 'smalldatetime' => self::TYPE_DATETIME, + 'datetime' => self::TYPE_DATETIME, + 'time' => self::TYPE_TIME, + + // character strings + 'char' => self::TYPE_STRING, + 'varchar' => self::TYPE_STRING, + 'text' => self::TYPE_TEXT, + + // unicode character strings + 'nchar' => self::TYPE_STRING, + 'nvarchar' => self::TYPE_STRING, + 'ntext' => self::TYPE_TEXT, + + // binary strings + 'binary' => self::TYPE_BINARY, + 'varbinary' => self::TYPE_BINARY, + 'image' => self::TYPE_BINARY, + + // other data types + // 'cursor' type cannot be used with tables + 'timestamp' => self::TYPE_TIMESTAMP, + 'hierarchyid' => self::TYPE_STRING, + 'uniqueidentifier' => self::TYPE_STRING, + 'sql_variant' => self::TYPE_STRING, + 'xml' => self::TYPE_STRING, + 'table' => self::TYPE_STRING, + ]; + + /** + * @inheritdoc + */ + public function createSavepoint($name) + { + $this->db->createCommand("SAVE TRANSACTION $name")->execute(); + } + + /** + * @inheritdoc + */ + public function releaseSavepoint($name) + { + // does nothing as MSSQL does not support this + } + + /** + * @inheritdoc + */ + public function rollBackSavepoint($name) + { + $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute(); + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name. + * @return string the properly quoted table name. + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '[') === false ? "[{$name}]" : $name; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name. + * @return string the properly quoted column name. + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '[') === false && $name !== '*' ? "[{$name}]" : $name; + } + + /** + * Creates a query builder for the MSSQL database. + * @return QueryBuilder query builder interface. + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + $this->findPrimaryKeys($table); + if ($this->findColumns($table)) { + $this->findForeignKeys($table); + + return $table; + } else { + return null; + } + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace(['[', ']'], '', $name)); + $partCount = count($parts); + if ($partCount == 3) { + // catalog name, schema name and table name passed + $table->catalogName = $parts[0]; + $table->schemaName = $parts[1]; + $table->name = $parts[2]; + $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name; + } elseif ($partCount == 2) { + // only schema name and table name passed + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; + } else { + // only table name passed + $table->schemaName = $this->defaultSchema; + $table->fullName = $table->name = $parts[0]; + } + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + + $column->name = $info['column_name']; + $column->allowNull = $info['is_nullable'] == 'YES'; + $column->dbType = $info['data_type']; + $column->enumValues = []; // mssql has only vague equivalents to enum + $column->isPrimaryKey = null; // primary key will be determined in findColumns() method + $column->autoIncrement = $info['is_identity'] == 1; + $column->unsigned = stripos($column->dbType, 'unsigned') !== false; + $column->comment = $info['comment'] === null ? '' : $info['comment']; + + $column->type = self::TYPE_STRING; + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int) $values[0]; + if (isset($values[1])) { + $column->scale = (int) $values[1]; + } + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + + $column->phpType = $this->getColumnPhpType($column); + + if ($info['column_default'] == '(NULL)') { + $info['column_default'] = null; + } + if ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP') { + $column->defaultValue = $column->typecast($info['column_default']); + } + + return $column; + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $columnsTableName = 'information_schema.columns'; + $whereSql = "[t1].[table_name] = '{$table->name}'"; + if ($table->catalogName !== null) { + $columnsTableName = "{$table->catalogName}.{$columnsTableName}"; + $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'"; + } + if ($table->schemaName !== null) { + $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'"; + } + $columnsTableName = $this->quoteTableName($columnsTableName); + + $sql = <<db->createCommand($sql)->queryAll(); - } catch (\Exception $e) { - return false; - } - foreach ($columns as $column) { - $column = $this->loadColumnSchema($column); - foreach ($table->primaryKey as $primaryKey) { - if (strcasecmp($column->name, $primaryKey) === 0) { - $column->isPrimaryKey = true; - break; - } - } - if ($column->isPrimaryKey && $column->autoIncrement) { - $table->sequenceName = ''; - } - $table->columns[$column->name] = $column; - } - return true; - } - - /** - * Collects the primary key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findPrimaryKeys($table) - { - $keyColumnUsageTableName = 'information_schema.key_column_usage'; - $tableConstraintsTableName = 'information_schema.table_constraints'; - if ($table->catalogName !== null) { - $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; - $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; - } - $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); - $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); - - $sql = <<db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + return false; + } + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + foreach ($table->primaryKey as $primaryKey) { + if (strcasecmp($column->name, $primaryKey) === 0) { + $column->isPrimaryKey = true; + break; + } + } + if ($column->isPrimaryKey && $column->autoIncrement) { + $table->sequenceName = ''; + } + $table->columns[$column->name] = $column; + } + + return true; + } + + /** + * Collects the primary key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findPrimaryKeys($table) + { + $keyColumnUsageTableName = 'information_schema.key_column_usage'; + $tableConstraintsTableName = 'information_schema.table_constraints'; + if ($table->catalogName !== null) { + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; + } + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); + + $sql = <<primaryKey = $this->db - ->createCommand($sql, [':tableName' => $table->name, ':schemaName' => $table->schemaName]) - ->queryColumn(); - } - - /** - * Collects the foreign key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findForeignKeys($table) - { - $referentialConstraintsTableName = 'information_schema.referential_constraints'; - $keyColumnUsageTableName = 'information_schema.key_column_usage'; - if ($table->catalogName !== null) { - $referentialConstraintsTableName = $table->catalogName . '.' . $referentialConstraintsTableName; - $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; - } - $referentialConstraintsTableName = $this->quoteTableName($referentialConstraintsTableName); - $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); - - // please refer to the following page for more details: - // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx - $sql = <<primaryKey = $this->db + ->createCommand($sql, [':tableName' => $table->name, ':schemaName' => $table->schemaName]) + ->queryColumn(); + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findForeignKeys($table) + { + $referentialConstraintsTableName = 'information_schema.referential_constraints'; + $keyColumnUsageTableName = 'information_schema.key_column_usage'; + if ($table->catalogName !== null) { + $referentialConstraintsTableName = $table->catalogName . '.' . $referentialConstraintsTableName; + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + } + $referentialConstraintsTableName = $this->quoteTableName($referentialConstraintsTableName); + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + + // please refer to the following page for more details: + // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx + $sql = <<db->createCommand($sql, [':tableName' => $table->name])->queryAll(); - $table->foreignKeys = []; - foreach ($rows as $row) { - $table->foreignKeys[] = [$row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']]; - } - } - - /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - if ($schema === '') { - $schema = $this->defaultSchema; - } - - $sql = <<db->createCommand($sql, [':tableName' => $table->name])->queryAll(); + $table->foreignKeys = []; + foreach ($rows as $row) { + $table->foreignKeys[] = [$row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']]; + } + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $schema = $this->defaultSchema; + } + + $sql = <<db->createCommand($sql, [':schema' => $schema])->queryColumn(); - } + return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); + } } diff --git a/framework/db/mssql/SqlsrvPDO.php b/framework/db/mssql/SqlsrvPDO.php index 29444c55caa..1f29b4a682f 100644 --- a/framework/db/mssql/SqlsrvPDO.php +++ b/framework/db/mssql/SqlsrvPDO.php @@ -16,18 +16,18 @@ */ class SqlsrvPDO extends \PDO { - /** - * Returns value of the last inserted ID. - * - * SQLSRV driver implements [[PDO::lastInsertId()]] method but with a single peculiarity: - * when `$sequence` value is a null or an empty string it returns an empty string. - * But when parameter is not specified it works as expected and returns actual - * last inserted ID (like the other PDO drivers). - * @param string|null $sequence the sequence name. Defaults to null. - * @return integer last inserted ID value. - */ - public function lastInsertId($sequence = null) - { - return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); - } + /** + * Returns value of the last inserted ID. + * + * SQLSRV driver implements [[PDO::lastInsertId()]] method but with a single peculiarity: + * when `$sequence` value is a null or an empty string it returns an empty string. + * But when parameter is not specified it works as expected and returns actual + * last inserted ID (like the other PDO drivers). + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); + } } diff --git a/framework/db/mssql/TableSchema.php b/framework/db/mssql/TableSchema.php index 67ad85c1c97..6c0d330342b 100644 --- a/framework/db/mssql/TableSchema.php +++ b/framework/db/mssql/TableSchema.php @@ -15,9 +15,9 @@ */ class TableSchema extends \yii\db\TableSchema { - /** - * @var string name of the catalog (database) that this table belongs to. - * Defaults to null, meaning no catalog (or the current database). - */ - public $catalogName; + /** + * @var string name of the catalog (database) that this table belongs to. + * Defaults to null, meaning no catalog (or the current database). + */ + public $catalogName; } diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index c8e8bb7dcce..16a12dc9308 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -18,147 +18,148 @@ */ class QueryBuilder extends \yii\db\QueryBuilder { - /** - * @var array mapping from abstract column types (keys) to physical column types (values). - */ - public $typeMap = [ - Schema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', - Schema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY', - Schema::TYPE_STRING => 'varchar(255)', - Schema::TYPE_TEXT => 'text', - Schema::TYPE_SMALLINT => 'smallint(6)', - Schema::TYPE_INTEGER => 'int(11)', - Schema::TYPE_BIGINT => 'bigint(20)', - Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal(10,0)', - Schema::TYPE_DATETIME => 'datetime', - Schema::TYPE_TIMESTAMP => 'timestamp', - Schema::TYPE_TIME => 'time', - Schema::TYPE_DATE => 'date', - Schema::TYPE_BINARY => 'blob', - Schema::TYPE_BOOLEAN => 'tinyint(1)', - Schema::TYPE_MONEY => 'decimal(19,4)', - ]; + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint(6)', + Schema::TYPE_INTEGER => 'int(11)', + Schema::TYPE_BIGINT => 'bigint(20)', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DECIMAL => 'decimal(10,0)', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'blob', + Schema::TYPE_BOOLEAN => 'tinyint(1)', + Schema::TYPE_MONEY => 'decimal(19,4)', + ]; - /** - * Builds a SQL statement for renaming a column. - * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. - * @param string $oldName the old name of the column. The name will be properly quoted by the method. - * @param string $newName the new name of the column. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB column. - * @throws Exception - */ - public function renameColumn($table, $oldName, $newName) - { - $quotedTable = $this->db->quoteTableName($table); - $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne(); - if ($row === false) { - throw new Exception("Unable to find column '$oldName' in table '$table'."); - } - if (isset($row['Create Table'])) { - $sql = $row['Create Table']; - } else { - $row = array_values($row); - $sql = $row[1]; - } - if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) { - foreach ($matches[1] as $i => $c) { - if ($c === $oldName) { - return "ALTER TABLE $quotedTable CHANGE " - . $this->db->quoteColumnName($oldName) . ' ' - . $this->db->quoteColumnName($newName) . ' ' - . $matches[2][$i]; - } - } - } - // try to give back a SQL anyway - return "ALTER TABLE $quotedTable CHANGE " - . $this->db->quoteColumnName($oldName) . ' ' - . $this->db->quoteColumnName($newName); - } + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $oldName the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + * @throws Exception + */ + public function renameColumn($table, $oldName, $newName) + { + $quotedTable = $this->db->quoteTableName($table); + $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne(); + if ($row === false) { + throw new Exception("Unable to find column '$oldName' in table '$table'."); + } + if (isset($row['Create Table'])) { + $sql = $row['Create Table']; + } else { + $row = array_values($row); + $sql = $row[1]; + } + if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) { + foreach ($matches[1] as $i => $c) { + if ($c === $oldName) { + return "ALTER TABLE $quotedTable CHANGE " + . $this->db->quoteColumnName($oldName) . ' ' + . $this->db->quoteColumnName($newName) . ' ' + . $matches[2][$i]; + } + } + } + // try to give back a SQL anyway + return "ALTER TABLE $quotedTable CHANGE " + . $this->db->quoteColumnName($oldName) . ' ' + . $this->db->quoteColumnName($newName); + } - /** - * Builds a SQL statement for dropping a foreign key constraint. - * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping a foreign key constraint. - */ - public function dropForeignKey($name, $table) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) - . ' DROP FOREIGN KEY ' . $this->db->quoteColumnName($name); - } + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a foreign key constraint. + */ + public function dropForeignKey($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) + . ' DROP FOREIGN KEY ' . $this->db->quoteColumnName($name); + } - /** - * Builds a SQL statement for removing a primary key constraint to an existing table. - * @param string $name the name of the primary key constraint to be removed. - * @param string $table the table that the primary key constraint will be removed from. - * @return string the SQL statement for removing a primary key constraint from an existing table. - */ - public function dropPrimaryKey($name, $table) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY'; - } + /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return string the SQL statement for removing a primary key constraint from an existing table. + */ + public function dropPrimaryKey($name, $table) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY'; + } - /** - * Creates a SQL statement for resetting the sequence value of a table's primary key. - * The sequence will be reset such that the primary key of the next new row inserted - * will have the specified value or 1. - * @param string $tableName the name of the table whose primary key sequence will be reset - * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, - * the next new row's primary key will have a value 1. - * @return string the SQL statement for resetting sequence - * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. - */ - public function resetSequence($tableName, $value = null) - { - $table = $this->db->getTableSchema($tableName); - if ($table !== null && $table->sequenceName !== null) { - $tableName = $this->db->quoteTableName($tableName); - if ($value === null) { - $key = reset($table->primaryKey); - $value = $this->db->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1; - } else { - $value = (int)$value; - } - return "ALTER TABLE $tableName AUTO_INCREMENT=$value"; - } elseif ($table === null) { - throw new InvalidParamException("Table not found: $tableName"); - } else { - throw new InvalidParamException("There is no sequence associated with table '$tableName'."); - } - } + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $tableName the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return string the SQL statement for resetting sequence + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function resetSequence($tableName, $value = null) + { + $table = $this->db->getTableSchema($tableName); + if ($table !== null && $table->sequenceName !== null) { + $tableName = $this->db->quoteTableName($tableName); + if ($value === null) { + $key = reset($table->primaryKey); + $value = $this->db->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1; + } else { + $value = (int) $value; + } - /** - * Builds a SQL statement for enabling or disabling integrity check. - * @param boolean $check whether to turn on or off the integrity check. - * @param string $table the table name. Meaningless for MySQL. - * @param string $schema the schema of the tables. Meaningless for MySQL. - * @return string the SQL statement for checking integrity - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0); - } + return "ALTER TABLE $tableName AUTO_INCREMENT=$value"; + } elseif ($table === null) { + throw new InvalidParamException("Table not found: $tableName"); + } else { + throw new InvalidParamException("There is no sequence associated with table '$tableName'."); + } + } - /** - * @inheritdoc - */ - public function buildLimit($limit, $offset) - { - $sql = ''; - if ($this->hasLimit($limit)) { - $sql = 'LIMIT ' . $limit; - if ($this->hasOffset($offset)) { - $sql .= ' OFFSET ' . $offset; - } - } elseif ($this->hasOffset($offset)) { - // limit is not optional in MySQL - // http://stackoverflow.com/a/271650/1106908 - // http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240 - $sql = "LIMIT $offset, 18446744073709551615"; // 2^64-1 - } + /** + * Builds a SQL statement for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $table the table name. Meaningless for MySQL. + * @param string $schema the schema of the tables. Meaningless for MySQL. + * @return string the SQL statement for checking integrity + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0); + } - return $sql; - } + /** + * @inheritdoc + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + if ($this->hasLimit($limit)) { + $sql = 'LIMIT ' . $limit; + if ($this->hasOffset($offset)) { + $sql .= ' OFFSET ' . $offset; + } + } elseif ($this->hasOffset($offset)) { + // limit is not optional in MySQL + // http://stackoverflow.com/a/271650/1106908 + // http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240 + $sql = "LIMIT $offset, 18446744073709551615"; // 2^64-1 + } + + return $sql; + } } diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 38f999a084c..0c19c66fc73 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -18,275 +18,279 @@ */ class Schema extends \yii\db\Schema { - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - */ - public $typeMap = [ - 'tinyint' => self::TYPE_SMALLINT, - 'bit' => self::TYPE_SMALLINT, - 'smallint' => self::TYPE_SMALLINT, - 'mediumint' => self::TYPE_INTEGER, - 'int' => self::TYPE_INTEGER, - 'integer' => self::TYPE_INTEGER, - 'bigint' => self::TYPE_BIGINT, - 'float' => self::TYPE_FLOAT, - 'double' => self::TYPE_FLOAT, - 'real' => self::TYPE_FLOAT, - 'decimal' => self::TYPE_DECIMAL, - 'numeric' => self::TYPE_DECIMAL, - 'tinytext' => self::TYPE_TEXT, - 'mediumtext' => self::TYPE_TEXT, - 'longtext' => self::TYPE_TEXT, - 'text' => self::TYPE_TEXT, - 'varchar' => self::TYPE_STRING, - 'string' => self::TYPE_STRING, - 'char' => self::TYPE_STRING, - 'datetime' => self::TYPE_DATETIME, - 'year' => self::TYPE_DATE, - 'date' => self::TYPE_DATE, - 'time' => self::TYPE_TIME, - 'timestamp' => self::TYPE_TIMESTAMP, - 'enum' => self::TYPE_STRING, - ]; + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = [ + 'tinyint' => self::TYPE_SMALLINT, + 'bit' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'mediumint' => self::TYPE_INTEGER, + 'int' => self::TYPE_INTEGER, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'float' => self::TYPE_FLOAT, + 'double' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + 'decimal' => self::TYPE_DECIMAL, + 'numeric' => self::TYPE_DECIMAL, + 'tinytext' => self::TYPE_TEXT, + 'mediumtext' => self::TYPE_TEXT, + 'longtext' => self::TYPE_TEXT, + 'text' => self::TYPE_TEXT, + 'varchar' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + 'char' => self::TYPE_STRING, + 'datetime' => self::TYPE_DATETIME, + 'year' => self::TYPE_DATE, + 'date' => self::TYPE_DATE, + 'time' => self::TYPE_TIME, + 'timestamp' => self::TYPE_TIMESTAMP, + 'enum' => self::TYPE_STRING, + ]; - /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteSimpleTableName($name) - { - return strpos($name, "`") !== false ? $name : "`" . $name . "`"; - } + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, "`") !== false ? $name : "`" . $name . "`"; + } - /** - * Quotes a column name for use in a query. - * A simple column name has no prefix. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; - } + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; + } - /** - * Creates a query builder for the MySQL database. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } + /** + * Creates a query builder for the MySQL database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function loadTableSchema($name) - { - $table = new TableSchema; - $this->resolveTableNames($table, $name); + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTableSchema($name) + { + $table = new TableSchema; + $this->resolveTableNames($table, $name); - if ($this->findColumns($table)) { - $this->findConstraints($table); - return $table; - } else { - return null; - } - } + if ($this->findColumns($table)) { + $this->findConstraints($table); - /** - * Resolves the table name and schema name (if any). - * @param TableSchema $table the table metadata object - * @param string $name the table name - */ - protected function resolveTableNames($table, $name) - { - $parts = explode('.', str_replace('`', '', $name)); - if (isset($parts[1])) { - $table->schemaName = $parts[0]; - $table->name = $parts[1]; - $table->fullName = $table->schemaName . '.' . $table->name; - } else { - $table->fullName = $table->name = $parts[0]; - } - } + return $table; + } else { + return null; + } + } - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema; + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('`', '', $name)); + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + $table->fullName = $table->schemaName . '.' . $table->name; + } else { + $table->fullName = $table->name = $parts[0]; + } + } - $column->name = $info['Field']; - $column->allowNull = $info['Null'] === 'YES'; - $column->isPrimaryKey = strpos($info['Key'], 'PRI') !== false; - $column->autoIncrement = stripos($info['Extra'], 'auto_increment') !== false; - $column->comment = $info['Comment']; + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema; + $column->name = $info['Field']; + $column->allowNull = $info['Null'] === 'YES'; + $column->isPrimaryKey = strpos($info['Key'], 'PRI') !== false; + $column->autoIncrement = stripos($info['Extra'], 'auto_increment') !== false; + $column->comment = $info['Comment']; - $column->dbType = $info['Type']; - $column->unsigned = strpos($column->dbType, 'unsigned') !== false; + $column->dbType = $info['Type']; + $column->unsigned = strpos($column->dbType, 'unsigned') !== false; - $column->type = self::TYPE_STRING; - if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { - $type = $matches[1]; - if (isset($this->typeMap[$type])) { - $column->type = $this->typeMap[$type]; - } - if (!empty($matches[2])) { - if ($type === 'enum') { - $values = explode(',', $matches[2]); - foreach ($values as $i => $value) { - $values[$i] = trim($value, "'"); - } - $column->enumValues = $values; - } else { - $values = explode(',', $matches[2]); - $column->size = $column->precision = (int)$values[0]; - if (isset($values[1])) { - $column->scale = (int)$values[1]; - } - if ($column->size === 1 && $type === 'bit') { - $column->type = 'boolean'; - } elseif ($type === 'bit') { - if ($column->size > 32) { - $column->type = 'bigint'; - } elseif ($column->size === 32) { - $column->type = 'integer'; - } - } - } - } - } + $column->type = self::TYPE_STRING; + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + if ($type === 'enum') { + $values = explode(',', $matches[2]); + foreach ($values as $i => $value) { + $values[$i] = trim($value, "'"); + } + $column->enumValues = $values; + } else { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int) $values[0]; + if (isset($values[1])) { + $column->scale = (int) $values[1]; + } + if ($column->size === 1 && $type === 'bit') { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + } - $column->phpType = $this->getColumnPhpType($column); + $column->phpType = $this->getColumnPhpType($column); - if ($column->type !== 'timestamp' || $info['Default'] !== 'CURRENT_TIMESTAMP') { - $column->defaultValue = $column->typecast($info['Default']); - } + if ($column->type !== 'timestamp' || $info['Default'] !== 'CURRENT_TIMESTAMP') { + $column->defaultValue = $column->typecast($info['Default']); + } - return $column; - } + return $column; + } - /** - * Collects the metadata of table columns. - * @param TableSchema $table the table metadata - * @return boolean whether the table exists in the database - * @throws \Exception if DB query fails - */ - protected function findColumns($table) - { - $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteSimpleTableName($table->name); - try { - $columns = $this->db->createCommand($sql)->queryAll(); - } catch (\Exception $e) { - $previous = $e->getPrevious(); - if ($previous instanceof \PDOException && $previous->getCode() == '42S02') { - // table does not exist - return false; - } - throw $e; - } - foreach ($columns as $info) { - $column = $this->loadColumnSchema($info); - $table->columns[$column->name] = $column; - if ($column->isPrimaryKey) { - $table->primaryKey[] = $column->name; - if ($column->autoIncrement) { - $table->sequenceName = ''; - } - } - } - return true; - } + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + * @throws \Exception if DB query fails + */ + protected function findColumns($table) + { + $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteSimpleTableName($table->name); + try { + $columns = $this->db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + $previous = $e->getPrevious(); + if ($previous instanceof \PDOException && $previous->getCode() == '42S02') { + // table does not exist + return false; + } + throw $e; + } + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey) { + $table->primaryKey[] = $column->name; + if ($column->autoIncrement) { + $table->sequenceName = ''; + } + } + } - /** - * Gets the CREATE TABLE sql string. - * @param TableSchema $table the table metadata - * @return string $sql the result of 'SHOW CREATE TABLE' - */ - protected function getCreateTableSql($table) - { - $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteSimpleTableName($table->name))->queryOne(); - if (isset($row['Create Table'])) { - $sql = $row['Create Table']; - } else { - $row = array_values($row); - $sql = $row[1]; - } - return $sql; - } + return true; + } - /** - * Collects the foreign key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findConstraints($table) - { - $sql = $this->getCreateTableSql($table); + /** + * Gets the CREATE TABLE sql string. + * @param TableSchema $table the table metadata + * @return string $sql the result of 'SHOW CREATE TABLE' + */ + protected function getCreateTableSql($table) + { + $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteSimpleTableName($table->name))->queryOne(); + if (isset($row['Create Table'])) { + $sql = $row['Create Table']; + } else { + $row = array_values($row); + $sql = $row[1]; + } - $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; - if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $fks = array_map('trim', explode(',', str_replace('`', '', $match[1]))); - $pks = array_map('trim', explode(',', str_replace('`', '', $match[3]))); - $constraint = [str_replace('`', '', $match[2])]; - foreach ($fks as $k => $name) { - $constraint[$name] = $pks[$k]; - } - $table->foreignKeys[] = $constraint; - } - } - } + return $sql; + } - /** - * Returns all unique indexes for the given table. - * Each array element is of the following structure: - * - * ~~~ - * [ - * 'IndexName1' => ['col1' [, ...]], - * 'IndexName2' => ['col2' [, ...]], - * ] - * ~~~ - * - * @param TableSchema $table the table metadata - * @return array all unique indexes for the given table. - */ - public function findUniqueIndexes($table) - { - $sql = $this->getCreateTableSql($table); - $uniqueIndexes = []; + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $sql = $this->getCreateTableSql($table); - $regexp = '/UNIQUE KEY\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; - if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $indexName = str_replace('`', '', $match[1]); - $indexColumns = array_map('trim', explode(',', str_replace('`', '', $match[2]))); - $uniqueIndexes[$indexName] = $indexColumns; - } - } - return $uniqueIndexes; - } + $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; + if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $fks = array_map('trim', explode(',', str_replace('`', '', $match[1]))); + $pks = array_map('trim', explode(',', str_replace('`', '', $match[3]))); + $constraint = [str_replace('`', '', $match[2])]; + foreach ($fks as $k => $name) { + $constraint[$name] = $pks[$k]; + } + $table->foreignKeys[] = $constraint; + } + } + } - /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - $sql = 'SHOW TABLES'; - if ($schema !== '') { - $sql .= ' FROM ' . $this->quoteSimpleTableName($schema); - } - return $this->db->createCommand($sql)->queryColumn(); - } + /** + * Returns all unique indexes for the given table. + * Each array element is of the following structure: + * + * ~~~ + * [ + * 'IndexName1' => ['col1' [, ...]], + * 'IndexName2' => ['col2' [, ...]], + * ] + * ~~~ + * + * @param TableSchema $table the table metadata + * @return array all unique indexes for the given table. + */ + public function findUniqueIndexes($table) + { + $sql = $this->getCreateTableSql($table); + $uniqueIndexes = []; + + $regexp = '/UNIQUE KEY\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; + if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $indexName = str_replace('`', '', $match[1]); + $indexColumns = array_map('trim', explode(',', str_replace('`', '', $match[2]))); + $uniqueIndexes[$indexName] = $indexColumns; + } + } + + return $uniqueIndexes; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + $sql = 'SHOW TABLES'; + if ($schema !== '') { + $sql .= ' FROM ' . $this->quoteSimpleTableName($schema); + } + + return $this->db->createCommand($sql)->queryColumn(); + } } diff --git a/framework/db/oci/QueryBuilder.php b/framework/db/oci/QueryBuilder.php index 2bec96ce907..1dc7147055f 100644 --- a/framework/db/oci/QueryBuilder.php +++ b/framework/db/oci/QueryBuilder.php @@ -18,118 +18,120 @@ class QueryBuilder extends \yii\db\QueryBuilder { - private $sql; - - public function build($query) - { - $params = $query->params; - $clauses = [ - $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption), - $this->buildFrom($query->from, $params), - $this->buildJoin($query->join, $params), - $this->buildWhere($query->where, $params), - $this->buildGroupBy($query->groupBy), - $this->buildHaving($query->having, $params), - $this->buildOrderBy($query->orderBy), - ]; - $this->sql = implode($this->separator, array_filter($clauses)); - - $this->sql = $this->buildLimit($query->limit, $query->offset); - - $unions = $this->buildUnion($query->union, $params); - if ($unions !== '') { - $this->sql .= $this->separator . $unions; - } - - return [$this->sql, $params]; - } - - public function buildLimit($limit, $offset) - { - $filters = []; - if ($this->hasOffset($offset) > 0) { - $filters[] = 'rowNumId > ' . $offset; - } - - if ($this->hasLimit($limit)) { - $filters[] = 'rownum <= ' . $limit; - } - - if (!empty($filters)) { - $filter = implode(' and ', $filters); - return <<params; + $clauses = [ + $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption), + $this->buildFrom($query->from, $params), + $this->buildJoin($query->join, $params), + $this->buildWhere($query->where, $params), + $this->buildGroupBy($query->groupBy), + $this->buildHaving($query->having, $params), + $this->buildOrderBy($query->orderBy), + ]; + $this->sql = implode($this->separator, array_filter($clauses)); + + $this->sql = $this->buildLimit($query->limit, $query->offset); + + $unions = $this->buildUnion($query->union, $params); + if ($unions !== '') { + $this->sql .= $this->separator . $unions; + } + + return [$this->sql, $params]; + } + + public function buildLimit($limit, $offset) + { + $filters = []; + if ($this->hasOffset($offset) > 0) { + $filters[] = 'rowNumId > ' . $offset; + } + + if ($this->hasLimit($limit)) { + $filters[] = 'rownum <= ' . $limit; + } + + if (!empty($filters)) { + $filter = implode(' and ', $filters); + + return <<sql}), - PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) + PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) SELECT * FROM PAGINATION WHERE $filter EOD; - } else { - return $this->sql; - } - } - - - /** - * Builds a SQL statement for renaming a DB table. - * - * @param string $table the table to be renamed. The name will be properly quoted by the method. - * @param string $newName the new table name. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB table. - */ - public function renameTable($table, $newName) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName); - } - - /** - * Builds a SQL statement for changing the definition of a column. - * - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) - * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. - * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. - * @return string the SQL statement for changing the definition of a column. - */ - public function alterColumn($table, $column, $type) - { - $type = $this->getColumnType($type); - return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' MODIFY ' . $this->db->quoteColumnName($column) . ' ' . $this->getColumnType($type); - } - - /** - * Builds a SQL statement for dropping an index. - * - * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping an index. - */ - public function dropIndex($name, $table) - { - return 'DROP INDEX ' . $this->db->quoteTableName($name); - } - - /** - * @inheritdoc - */ - public function resetSequence($table, $value = null) - { - $tableSchema = $this->db->getTableSchema($table); - if ($tableSchema === null) { - throw new InvalidParamException("Unknown table: $table"); - } - if ($tableSchema->sequenceName === null) { - return ''; - } - - if ($value !== null) { - $value = (int)$value; - } else { - $value = (int)$this->db->createCommand("SELECT MAX(\"{$tableSchema->primaryKey}\") FROM \"{$tableSchema->name}\"")->queryScalar(); - $value++; - } - return "DROP SEQUENCE \"{$tableSchema->name}_SEQ\";" - . "CREATE SEQUENCE \"{$tableSchema->name}_SEQ\" START WITH {$value} INCREMENT BY 1 NOMAXVALUE NOCACHE"; - } + } else { + return $this->sql; + } + } + + /** + * Builds a SQL statement for renaming a DB table. + * + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($table, $newName) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + $type = $this->getColumnType($type); + + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' MODIFY ' . $this->db->quoteColumnName($column) . ' ' . $this->getColumnType($type); + } + + /** + * Builds a SQL statement for dropping an index. + * + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX ' . $this->db->quoteTableName($name); + } + + /** + * @inheritdoc + */ + public function resetSequence($table, $value = null) + { + $tableSchema = $this->db->getTableSchema($table); + if ($tableSchema === null) { + throw new InvalidParamException("Unknown table: $table"); + } + if ($tableSchema->sequenceName === null) { + return ''; + } + + if ($value !== null) { + $value = (int) $value; + } else { + $value = (int) $this->db->createCommand("SELECT MAX(\"{$tableSchema->primaryKey}\") FROM \"{$tableSchema->name}\"")->queryScalar(); + $value++; + } + + return "DROP SEQUENCE \"{$tableSchema->name}_SEQ\";" + . "CREATE SEQUENCE \"{$tableSchema->name}_SEQ\" START WITH {$value} INCREMENT BY 1 NOMAXVALUE NOCACHE"; + } } diff --git a/framework/db/oci/Schema.php b/framework/db/oci/Schema.php index 1edbf6b6175..6fe42f85e97 100644 --- a/framework/db/oci/Schema.php +++ b/framework/db/oci/Schema.php @@ -20,96 +20,97 @@ */ class Schema extends \yii\db\Schema { - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->defaultSchema === null) { - $this->defaultSchema = $this->db->username; - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->defaultSchema === null) { + $this->defaultSchema = $this->db->username; + } + } - /** - * @inheritdoc - */ - public function releaseSavepoint($name) - { - // does nothing as Oracle does not support this - } + /** + * @inheritdoc + */ + public function releaseSavepoint($name) + { + // does nothing as Oracle does not support this + } - /** - * @inheritdoc - */ - public function quoteSimpleTableName($name) - { - return '"' . $name . '"'; - } + /** + * @inheritdoc + */ + public function quoteSimpleTableName($name) + { + return '"' . $name . '"'; + } - /** - * @inheritdoc - */ - public function quoteSimpleColumnName($name) - { - return '"' . $name . '"'; - } + /** + * @inheritdoc + */ + public function quoteSimpleColumnName($name) + { + return '"' . $name . '"'; + } - /** - * @inheritdoc - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } + /** + * @inheritdoc + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } - /** - * @inheritdoc - */ - public function loadTableSchema($name) - { - $table = new TableSchema(); - $this->resolveTableNames($table, $name); + /** + * @inheritdoc + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); - if ($this->findColumns($table)) { - $this->findConstraints($table); - return $table; - } else { - return null; - } - } + if ($this->findColumns($table)) { + $this->findConstraints($table); - /** - * Resolves the table name and schema name (if any). - * - * @param TableSchema $table the table metadata object - * @param string $name the table name - */ - protected function resolveTableNames($table, $name) - { - $parts = explode('.', str_replace('"', '', $name)); - if (isset($parts[1])) { - $table->schemaName = $parts[0]; - $table->name = $parts[1]; - } else { - $table->schemaName = $this->defaultSchema; - $table->name = $name; - } + return $table; + } else { + return null; + } + } - $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; - } + /** + * Resolves the table name and schema name (if any). + * + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('"', '', $name)); + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + $table->schemaName = $this->defaultSchema; + $table->name = $name; + } - /** - * Collects the table column metadata. - * @param TableSchema $table the table schema - * @return boolean whether the table exists - */ - protected function findColumns($table) - { - $schemaName = $table->schemaName; - $tableName = $table->name; + $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; + } - $sql = <<schemaName; + $tableName = $table->name; + + $sql = <<db->createCommand($sql)->queryAll(); - } catch (\Exception $e) { - return false; - } + try { + $columns = $this->db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + return false; + } + + foreach ($columns as $column) { + $c = $this->createColumn($column); + $table->columns[$c->name] = $c; + if ($c->isPrimaryKey) { + $table->primaryKey[] = $c->name; + $table->sequenceName = ''; + $c->autoIncrement = true; + } + } - foreach ($columns as $column) { - $c = $this->createColumn($column); - $table->columns[$c->name] = $c; - if ($c->isPrimaryKey) { - $table->primaryKey[] = $c->name; - $table->sequenceName = ''; - $c->autoIncrement = true; - } - } - return true; - } + return true; + } - protected function createColumn($column) - { - $c = new ColumnSchema(); - $c->name = $column['COLUMN_NAME']; - $c->allowNull = $column['NULLABLE'] === 'Y'; - $c->isPrimaryKey = strpos($column['KEY'], 'P') !== false; - $c->comment = $column['COLUMN_COMMENT'] === null ? '' : $column['COLUMN_COMMENT']; + protected function createColumn($column) + { + $c = new ColumnSchema(); + $c->name = $column['COLUMN_NAME']; + $c->allowNull = $column['NULLABLE'] === 'Y'; + $c->isPrimaryKey = strpos($column['KEY'], 'P') !== false; + $c->comment = $column['COLUMN_COMMENT'] === null ? '' : $column['COLUMN_COMMENT']; - $this->extractColumnType($c, $column['DATA_TYPE']); - $this->extractColumnSize($c, $column['DATA_TYPE']); + $this->extractColumnType($c, $column['DATA_TYPE']); + $this->extractColumnSize($c, $column['DATA_TYPE']); - if (stripos($column['DATA_DEFAULT'], 'timestamp') !== false) { - $c->defaultValue = null; - } else { - $c->defaultValue = $c->typecast($column['DATA_DEFAULT']); - } + if (stripos($column['DATA_DEFAULT'], 'timestamp') !== false) { + $c->defaultValue = null; + } else { + $c->defaultValue = $c->typecast($column['DATA_DEFAULT']); + } - return $c; - } + return $c; + } - protected function findConstraints($table) - { - $sql = << 'P' order by d.constraint_name, c.position EOD; - $command = $this->db->createCommand($sql); - foreach ($command->queryAll() as $row) { - if ($row['CONSTRAINT_TYPE'] === 'R') { - $name = $row["COLUMN_NAME"]; - $table->foreignKeys[$name] = [$row["TABLE_REF"], $row["COLUMN_REF"]]; - } - } - } + $command = $this->db->createCommand($sql); + foreach ($command->queryAll() as $row) { + if ($row['CONSTRAINT_TYPE'] === 'R') { + $name = $row["COLUMN_NAME"]; + $table->foreignKeys[$name] = [$row["TABLE_REF"], $row["COLUMN_REF"]]; + } + } + } - /** - * @inheritdoc - */ - protected function findTableNames($schema = '') - { - if ($schema === '') { - $sql = <<db->createCommand($sql); - } else { - $sql = <<db->createCommand($sql); + } else { + $sql = <<db->createCommand($sql); - $command->bindParam(':schema', $schema); - } + $command = $this->db->createCommand($sql); + $command->bindParam(':schema', $schema); + } + + $rows = $command->queryAll(); + $names = []; + foreach ($rows as $row) { + $names[] = $row['TABLE_NAME']; + } - $rows = $command->queryAll(); - $names = []; - foreach ($rows as $row) { - $names[] = $row['TABLE_NAME']; - } - return $names; - } + return $names; + } - /** - * Extracts the data types for the given column - * @param ColumnSchema $column - * @param string $dbType DB type - */ - protected function extractColumnType($column, $dbType) - { - $column->dbType = $dbType; + /** + * Extracts the data types for the given column + * @param ColumnSchema $column + * @param string $dbType DB type + */ + protected function extractColumnType($column, $dbType) + { + $column->dbType = $dbType; - if (strpos($dbType, 'FLOAT') !== false) { - $column->type = 'double'; - } elseif (strpos($dbType, 'NUMBER') !== false || strpos($dbType, 'INTEGER') !== false) { - if (strpos($dbType, '(') && preg_match('/\((.*)\)/', $dbType, $matches)) { - $values = explode(',', $matches[1]); - if (isset($values[1]) && (((int)$values[1]) > 0)) { - $column->type = 'double'; - } else { - $column->type = 'integer'; - } - } else { - $column->type = 'double'; - } - } else { - $column->type = 'string'; - } - } + if (strpos($dbType, 'FLOAT') !== false) { + $column->type = 'double'; + } elseif (strpos($dbType, 'NUMBER') !== false || strpos($dbType, 'INTEGER') !== false) { + if (strpos($dbType, '(') && preg_match('/\((.*)\)/', $dbType, $matches)) { + $values = explode(',', $matches[1]); + if (isset($values[1]) && (((int) $values[1]) > 0)) { + $column->type = 'double'; + } else { + $column->type = 'integer'; + } + } else { + $column->type = 'double'; + } + } else { + $column->type = 'string'; + } + } - /** - * Extracts size, precision and scale information from column's DB type. - * @param ColumnSchema $column - * @param string $dbType the column's DB type - */ - protected function extractColumnSize($column, $dbType) - { - if (strpos($dbType, '(') && preg_match('/\((.*)\)/', $dbType, $matches)) { - $values = explode(',', $matches[1]); - $column->size = $column->precision = (int)$values[0]; - if (isset($values[1])) { - $column->scale = (int)$values[1]; - } - } - } + /** + * Extracts size, precision and scale information from column's DB type. + * @param ColumnSchema $column + * @param string $dbType the column's DB type + */ + protected function extractColumnSize($column, $dbType) + { + if (strpos($dbType, '(') && preg_match('/\((.*)\)/', $dbType, $matches)) { + $values = explode(',', $matches[1]); + $column->size = $column->precision = (int) $values[0]; + if (isset($values[1])) { + $column->scale = (int) $values[1]; + } + } + } } diff --git a/framework/db/pgsql/QueryBuilder.php b/framework/db/pgsql/QueryBuilder.php index 998e7469aeb..932653d8949 100644 --- a/framework/db/pgsql/QueryBuilder.php +++ b/framework/db/pgsql/QueryBuilder.php @@ -19,123 +19,125 @@ class QueryBuilder extends \yii\db\QueryBuilder { - /** - * @var array mapping from abstract column types (keys) to physical column types (values). - */ - public $typeMap = [ - Schema::TYPE_PK => 'serial NOT NULL PRIMARY KEY', - Schema::TYPE_BIGPK => 'bigserial NOT NULL PRIMARY KEY', - Schema::TYPE_STRING => 'varchar(255)', - Schema::TYPE_TEXT => 'text', - Schema::TYPE_SMALLINT => 'smallint', - Schema::TYPE_INTEGER => 'integer', - Schema::TYPE_BIGINT => 'bigint', - Schema::TYPE_FLOAT => 'double precision', - Schema::TYPE_DECIMAL => 'numeric(10,0)', - Schema::TYPE_DATETIME => 'timestamp', - Schema::TYPE_TIMESTAMP => 'timestamp', - Schema::TYPE_TIME => 'time', - Schema::TYPE_DATE => 'date', - Schema::TYPE_BINARY => 'bytea', - Schema::TYPE_BOOLEAN => 'boolean', - Schema::TYPE_MONEY => 'numeric(19,4)', - ]; - - /** - * Builds a SQL statement for dropping an index. - * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping an index. - */ - public function dropIndex($name, $table) - { - return 'DROP INDEX ' . $this->db->quoteTableName($name); - } - - /** - * Builds a SQL statement for renaming a DB table. - * @param string $oldName the table to be renamed. The name will be properly quoted by the method. - * @param string $newName the new table name. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB table. - */ - public function renameTable($oldName, $newName) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO ' . $this->db->quoteTableName($newName); - } - - /** - * Creates a SQL statement for resetting the sequence value of a table's primary key. - * The sequence will be reset such that the primary key of the next new row inserted - * will have the specified value or 1. - * @param string $tableName the name of the table whose primary key sequence will be reset - * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, - * the next new row's primary key will have a value 1. - * @return string the SQL statement for resetting sequence - * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. - */ - public function resetSequence($tableName, $value = null) - { - $table = $this->db->getTableSchema($tableName); - if ($table !== null && $table->sequenceName !== null) { - $sequence = '"' . $table->sequenceName . '"'; - - if (strpos($sequence, '.') !== false) { - $sequence = str_replace('.', '"."', $sequence); - } - - $tableName = $this->db->quoteTableName($tableName); - if ($value === null) { - $key = reset($table->primaryKey); - $value = "(SELECT COALESCE(MAX(\"{$key}\"),0) FROM {$tableName})+1"; - } else { - $value = (int)$value; - } - return "SELECT SETVAL('$sequence',$value,false)"; - } elseif ($table === null) { - throw new InvalidParamException("Table not found: $tableName"); - } else { - throw new InvalidParamException("There is not sequence associated with table '$tableName'."); - } - } - - /** - * Builds a SQL statement for enabling or disabling integrity check. - * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. - * @param string $table the table name. - * @return string the SQL statement for checking integrity - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - $enable = $check ? 'ENABLE' : 'DISABLE'; - $schema = $schema ? $schema : $this->db->schema->defaultSchema; - $tableNames = $table ? [$table] : $this->db->schema->getTableNames($schema); - $command = ''; - - foreach ($tableNames as $tableName) { - $tableName = '"' . $schema . '"."' . $tableName . '"'; - $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; "; - } - - #enable to have ability to alter several tables - $this->db->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); - return $command; - } - - /** - * Builds a SQL statement for changing the definition of a column. - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract - * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept - * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' - * will become 'varchar(255) not null'. - * @return string the SQL statement for changing the definition of a column. - */ - public function alterColumn($table, $column, $type) - { - return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' - . $this->db->quoteColumnName($column) . ' TYPE ' - . $this->getColumnType($type); - } + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'serial NOT NULL PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigserial NOT NULL PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'integer', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'double precision', + Schema::TYPE_DECIMAL => 'numeric(10,0)', + Schema::TYPE_DATETIME => 'timestamp', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'bytea', + Schema::TYPE_BOOLEAN => 'boolean', + Schema::TYPE_MONEY => 'numeric(19,4)', + ]; + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX ' . $this->db->quoteTableName($name); + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $oldName the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($oldName, $newName) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO ' . $this->db->quoteTableName($newName); + } + + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $tableName the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return string the SQL statement for resetting sequence + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function resetSequence($tableName, $value = null) + { + $table = $this->db->getTableSchema($tableName); + if ($table !== null && $table->sequenceName !== null) { + $sequence = '"' . $table->sequenceName . '"'; + + if (strpos($sequence, '.') !== false) { + $sequence = str_replace('.', '"."', $sequence); + } + + $tableName = $this->db->quoteTableName($tableName); + if ($value === null) { + $key = reset($table->primaryKey); + $value = "(SELECT COALESCE(MAX(\"{$key}\"),0) FROM {$tableName})+1"; + } else { + $value = (int) $value; + } + + return "SELECT SETVAL('$sequence',$value,false)"; + } elseif ($table === null) { + throw new InvalidParamException("Table not found: $tableName"); + } else { + throw new InvalidParamException("There is not sequence associated with table '$tableName'."); + } + } + + /** + * Builds a SQL statement for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. + * @param string $table the table name. + * @return string the SQL statement for checking integrity + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + $enable = $check ? 'ENABLE' : 'DISABLE'; + $schema = $schema ? $schema : $this->db->schema->defaultSchema; + $tableNames = $table ? [$table] : $this->db->schema->getTableNames($schema); + $command = ''; + + foreach ($tableNames as $tableName) { + $tableName = '"' . $schema . '"."' . $tableName . '"'; + $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; "; + } + + #enable to have ability to alter several tables + $this->db->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); + + return $command; + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract + * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept + * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' + * will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' + . $this->db->quoteColumnName($column) . ' TYPE ' + . $this->getColumnType($type); + } } diff --git a/framework/db/pgsql/Schema.php b/framework/db/pgsql/Schema.php index b13fdf7a2ae..92c1a4af025 100644 --- a/framework/db/pgsql/Schema.php +++ b/framework/db/pgsql/Schema.php @@ -20,229 +20,232 @@ */ class Schema extends \yii\db\Schema { - /** - * @var string the default schema used for the current session. - */ - public $defaultSchema = 'public'; - /** - * @var array mapping from physical column types (keys) to abstract - * column types (values) - */ - public $typeMap = [ - 'abstime' => self::TYPE_TIMESTAMP, - 'bit' => self::TYPE_STRING, - 'bool' => self::TYPE_BOOLEAN, - 'boolean' => self::TYPE_BOOLEAN, - 'box' => self::TYPE_STRING, - 'character' => self::TYPE_STRING, - 'bytea' => self::TYPE_BINARY, - 'char' => self::TYPE_STRING, - 'cidr' => self::TYPE_STRING, - 'circle' => self::TYPE_STRING, - 'date' => self::TYPE_DATE, - 'real' => self::TYPE_FLOAT, - 'decimal' => self::TYPE_DECIMAL, - 'double precision' => self::TYPE_DECIMAL, - 'inet' => self::TYPE_STRING, - 'smallint' => self::TYPE_SMALLINT, - 'int4' => self::TYPE_INTEGER, - 'int8' => self::TYPE_BIGINT, - 'integer' => self::TYPE_INTEGER, - 'bigint' => self::TYPE_BIGINT, - 'interval' => self::TYPE_STRING, - 'json' => self::TYPE_STRING, - 'line' => self::TYPE_STRING, - 'macaddr' => self::TYPE_STRING, - 'money' => self::TYPE_MONEY, - 'name' => self::TYPE_STRING, - 'numeric' => self::TYPE_STRING, - 'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal! - 'path' => self::TYPE_STRING, - 'point' => self::TYPE_STRING, - 'polygon' => self::TYPE_STRING, - 'text' => self::TYPE_TEXT, - 'time without time zone' => self::TYPE_TIME, - 'timestamp without time zone' => self::TYPE_TIMESTAMP, - 'timestamp with time zone' => self::TYPE_TIMESTAMP, - 'time with time zone' => self::TYPE_TIMESTAMP, - 'unknown' => self::TYPE_STRING, - 'uuid' => self::TYPE_STRING, - 'bit varying' => self::TYPE_STRING, - 'character varying' => self::TYPE_STRING, - 'xml' => self::TYPE_STRING - ]; - - /** - * Creates a query builder for the PostgreSQL database. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Resolves the table name and schema name (if any). - * @param TableSchema $table the table metadata object - * @param string $name the table name - */ - protected function resolveTableNames($table, $name) - { - $parts = explode('.', str_replace('"', '', $name)); - - if (isset($parts[1])) { - $table->schemaName = $parts[0]; - $table->name = $parts[1]; - } else { - $table->schemaName = $this->defaultSchema; - $table->name = $name; - } - - $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; - } - - /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteSimpleTableName($name) - { - return strpos($name, '"') !== false ? $name : '"' . $name . '"'; - } - - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. - */ - public function loadTableSchema($name) - { - $table = new TableSchema(); - $this->resolveTableNames($table, $name); - if ($this->findColumns($table)) { - $this->findConstraints($table); - return $table; - } else { - return null; - } - } - - /** - * Determines the PDO type for the given PHP data value. - * @param mixed $data the data whose PDO type is to be determined - * @return integer the PDO type - * @see http://www.php.net/manual/en/pdo.constants.php - */ - public function getPdoType($data) - { - // php type => PDO type - static $typeMap = [ - // https://github.com/yiisoft/yii2/issues/1115 - // Cast boolean to integer values to work around problems with PDO casting false to string '' https://bugs.php.net/bug.php?id=33876 - 'boolean' => \PDO::PARAM_INT, - 'integer' => \PDO::PARAM_INT, - 'string' => \PDO::PARAM_STR, - 'resource' => \PDO::PARAM_LOB, - 'NULL' => \PDO::PARAM_NULL, - ]; - $type = gettype($data); - return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; - } - - /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - if ($schema === '') { - $schema = $this->defaultSchema; - } - $sql = << self::TYPE_TIMESTAMP, + 'bit' => self::TYPE_STRING, + 'bool' => self::TYPE_BOOLEAN, + 'boolean' => self::TYPE_BOOLEAN, + 'box' => self::TYPE_STRING, + 'character' => self::TYPE_STRING, + 'bytea' => self::TYPE_BINARY, + 'char' => self::TYPE_STRING, + 'cidr' => self::TYPE_STRING, + 'circle' => self::TYPE_STRING, + 'date' => self::TYPE_DATE, + 'real' => self::TYPE_FLOAT, + 'decimal' => self::TYPE_DECIMAL, + 'double precision' => self::TYPE_DECIMAL, + 'inet' => self::TYPE_STRING, + 'smallint' => self::TYPE_SMALLINT, + 'int4' => self::TYPE_INTEGER, + 'int8' => self::TYPE_BIGINT, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'interval' => self::TYPE_STRING, + 'json' => self::TYPE_STRING, + 'line' => self::TYPE_STRING, + 'macaddr' => self::TYPE_STRING, + 'money' => self::TYPE_MONEY, + 'name' => self::TYPE_STRING, + 'numeric' => self::TYPE_STRING, + 'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal! + 'path' => self::TYPE_STRING, + 'point' => self::TYPE_STRING, + 'polygon' => self::TYPE_STRING, + 'text' => self::TYPE_TEXT, + 'time without time zone' => self::TYPE_TIME, + 'timestamp without time zone' => self::TYPE_TIMESTAMP, + 'timestamp with time zone' => self::TYPE_TIMESTAMP, + 'time with time zone' => self::TYPE_TIMESTAMP, + 'unknown' => self::TYPE_STRING, + 'uuid' => self::TYPE_STRING, + 'bit varying' => self::TYPE_STRING, + 'character varying' => self::TYPE_STRING, + 'xml' => self::TYPE_STRING + ]; + + /** + * Creates a query builder for the PostgreSQL database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('"', '', $name)); + + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + $table->schemaName = $this->defaultSchema; + $table->name = $name; + } + + $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + if ($this->findColumns($table)) { + $this->findConstraints($table); + + return $table; + } else { + return null; + } + } + + /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + // php type => PDO type + static $typeMap = [ + // https://github.com/yiisoft/yii2/issues/1115 + // Cast boolean to integer values to work around problems with PDO casting false to string '' https://bugs.php.net/bug.php?id=33876 + 'boolean' => \PDO::PARAM_INT, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $schema = $this->defaultSchema; + } + $sql = <<db->createCommand($sql); - $command->bindParam(':schema', $schema); - $rows = $command->queryAll(); - $names = []; - foreach ($rows as $row) { - $names[] = $row['table_name']; - } - return $names; - } - - /** - * Collects the foreign key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findConstraints($table) - { - - $tableName = $this->quoteValue($table->name); - $tableSchema = $this->quoteValue($table->schemaName); - - //We need to extract the constraints de hard way since: - //http://www.postgresql.org/message-id/26677.1086673982@sss.pgh.pa.us - - $sql = <<db->createCommand($sql); + $command->bindParam(':schema', $schema); + $rows = $command->queryAll(); + $names = []; + foreach ($rows as $row) { + $names[] = $row['table_name']; + } + + return $names; + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + + $tableName = $this->quoteValue($table->name); + $tableSchema = $this->quoteValue($table->schemaName); + + //We need to extract the constraints de hard way since: + //http://www.postgresql.org/message-id/26677.1086673982@sss.pgh.pa.us + + $sql = <<db->createCommand($sql)->queryAll(); - foreach ($constraints as $constraint) { - $columns = explode(',', $constraint['columns']); - $fcolumns = explode(',', $constraint['foreign_columns']); - if ($constraint['foreign_table_schema'] !== $this->defaultSchema) { - $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name']; - } else { - $foreignTable = $constraint['foreign_table_name']; - } - $citem = [$foreignTable]; - foreach ($columns as $idx => $column) { - $citem[$column] = $fcolumns[$idx]; - } - $table->foreignKeys[] = $citem; - } - } - - /** - * Gets information about given table unique indexes. - * @param TableSchema $table the table metadata - * @return array with index names, columns and if it is an expression tree - */ - protected function getUniqueIndexInformation($table) - { - $tableName = $this->quoteValue($table->name); - $tableSchema = $this->quoteValue($table->schemaName); - - $sql = <<db->createCommand($sql)->queryAll(); + foreach ($constraints as $constraint) { + $columns = explode(',', $constraint['columns']); + $fcolumns = explode(',', $constraint['foreign_columns']); + if ($constraint['foreign_table_schema'] !== $this->defaultSchema) { + $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name']; + } else { + $foreignTable = $constraint['foreign_table_name']; + } + $citem = [$foreignTable]; + foreach ($columns as $idx => $column) { + $citem[$column] = $fcolumns[$idx]; + } + $table->foreignKeys[] = $citem; + } + } + + /** + * Gets information about given table unique indexes. + * @param TableSchema $table the table metadata + * @return array with index names, columns and if it is an expression tree + */ + protected function getUniqueIndexInformation($table) + { + $tableName = $this->quoteValue($table->name); + $tableSchema = $this->quoteValue($table->schemaName); + + $sql = <<db->createCommand($sql)->queryAll(); - } - - /** - * Returns all unique indexes for the given table. - * Each array element is of the following structure: - * - * ~~~ - * [ - * 'IndexName1' => ['col1' [, ...]], - * 'IndexName2' => ['col2' [, ...]], - * ] - * ~~~ - * - * @param TableSchema $table the table metadata - * @return array all unique indexes for the given table. - */ - public function findUniqueIndexes($table) - { - $indexes = $this->getUniqueIndexInformation($table); - $uniqueIndexes = []; - - foreach ($indexes as $index) { - $indexName = $index['indexname']; - - if ($index['indexprs']) { - // Index is an expression like "lower(colname::text)" - $indexColumns = preg_replace("/.*\(([^\:]+).*/mi", "$1", $index['indexcolumns']); - } else { - $indexColumns = array_map('trim', explode(',', str_replace(['{', '}', '"', '\\'], '', $index['indexcolumns']))); - } - - $uniqueIndexes[$indexName] = $indexColumns; - - } - return $uniqueIndexes; - } - - /** - * Collects the metadata of table columns. - * @param TableSchema $table the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - $tableName = $this->db->quoteValue($table->name); - $schemaName = $this->db->quoteValue($table->schemaName); - $sql = <<db->createCommand($sql)->queryAll(); + } + + /** + * Returns all unique indexes for the given table. + * Each array element is of the following structure: + * + * ~~~ + * [ + * 'IndexName1' => ['col1' [, ...]], + * 'IndexName2' => ['col2' [, ...]], + * ] + * ~~~ + * + * @param TableSchema $table the table metadata + * @return array all unique indexes for the given table. + */ + public function findUniqueIndexes($table) + { + $indexes = $this->getUniqueIndexInformation($table); + $uniqueIndexes = []; + + foreach ($indexes as $index) { + $indexName = $index['indexname']; + + if ($index['indexprs']) { + // Index is an expression like "lower(colname::text)" + $indexColumns = preg_replace("/.*\(([^\:]+).*/mi", "$1", $index['indexcolumns']); + } else { + $indexColumns = array_map('trim', explode(',', str_replace(['{', '}', '"', '\\'], '', $index['indexcolumns']))); + } + + $uniqueIndexes[$indexName] = $indexColumns; + + } + + return $uniqueIndexes; + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $tableName = $this->db->quoteValue($table->name); + $schemaName = $this->db->quoteValue($table->schemaName); + $sql = <<> 16) & 65535 - END - WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/ - WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/ - ELSE null - END AS numeric_precision, - CASE - WHEN atttypid IN (21, 23, 20) THEN 0 - WHEN atttypid IN (1700) THEN - CASE - WHEN atttypmod = -1 THEN null - ELSE (atttypmod - 4) & 65535 - END - ELSE null - END AS numeric_scale, - CAST( + d.nspname AS table_schema, + c.relname AS table_name, + a.attname AS column_name, + t.typname AS data_type, + a.attlen AS character_maximum_length, + pg_catalog.col_description(c.oid, a.attnum) AS column_comment, + a.atttypmod AS modifier, + a.attnotnull = false AS is_nullable, + CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default, + coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) AS is_autoinc, + array_to_string((select array_agg(enumlabel) from pg_enum where enumtypid=a.atttypid)::varchar[],',') as enum_values, + CASE atttypid + WHEN 21 /*int2*/ THEN 16 + WHEN 23 /*int4*/ THEN 32 + WHEN 20 /*int8*/ THEN 64 + WHEN 1700 /*numeric*/ THEN + CASE WHEN atttypmod = -1 + THEN null + ELSE ((atttypmod - 4) >> 16) & 65535 + END + WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/ + WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/ + ELSE null + END AS numeric_precision, + CASE + WHEN atttypid IN (21, 23, 20) THEN 0 + WHEN atttypid IN (1700) THEN + CASE + WHEN atttypmod = -1 THEN null + ELSE (atttypmod - 4) & 65535 + END + ELSE null + END AS numeric_scale, + CAST( information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t)) AS numeric - ) AS size, - a.attnum = any (ct.conkey) as is_pkey + ) AS size, + a.attnum = any (ct.conkey) as is_pkey FROM - pg_class c - LEFT JOIN pg_attribute a ON a.attrelid = c.oid - LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum - LEFT JOIN pg_type t ON a.atttypid = t.oid - LEFT JOIN pg_namespace d ON d.oid = c.relnamespace - LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p' + pg_class c + LEFT JOIN pg_attribute a ON a.attrelid = c.oid + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN pg_namespace d ON d.oid = c.relnamespace + LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p' WHERE - a.attnum > 0 and t.typname != '' - and c.relname = {$tableName} - and d.nspname = {$schemaName} + a.attnum > 0 and t.typname != '' + and c.relname = {$tableName} + and d.nspname = {$schemaName} ORDER BY - a.attnum; + a.attnum; SQL; - $columns = $this->db->createCommand($sql)->queryAll(); - if (empty($columns)) { - return false; - } - foreach ($columns as $column) { - $column = $this->loadColumnSchema($column); - $table->columns[$column->name] = $column; - if ($column->isPrimaryKey === true) { - $table->primaryKey[] = $column->name; - if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { - $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue); - } - } - } - return true; - } - - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema(); - $column->allowNull = $info['is_nullable']; - $column->autoIncrement = $info['is_autoinc']; - $column->comment = $info['column_comment']; - $column->dbType = $info['data_type']; - $column->defaultValue = $info['column_default']; - $column->enumValues = explode(',', str_replace(["''"], ["'"], $info['enum_values'])); - $column->unsigned = false; // has no meaning in PG - $column->isPrimaryKey = $info['is_pkey']; - $column->name = $info['column_name']; - $column->precision = $info['numeric_precision']; - $column->scale = $info['numeric_scale']; - $column->size = $info['size']; - - if (isset($this->typeMap[$column->dbType])) { - $column->type = $this->typeMap[$column->dbType]; - } else { - $column->type = self::TYPE_STRING; - } - $column->phpType = $this->getColumnPhpType($column); - return $column; - } + $columns = $this->db->createCommand($sql)->queryAll(); + if (empty($columns)) { + return false; + } + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey === true) { + $table->primaryKey[] = $column->name; + if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { + $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue); + } + } + } + + return true; + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + $column->allowNull = $info['is_nullable']; + $column->autoIncrement = $info['is_autoinc']; + $column->comment = $info['column_comment']; + $column->dbType = $info['data_type']; + $column->defaultValue = $info['column_default']; + $column->enumValues = explode(',', str_replace(["''"], ["'"], $info['enum_values'])); + $column->unsigned = false; // has no meaning in PG + $column->isPrimaryKey = $info['is_pkey']; + $column->name = $info['column_name']; + $column->precision = $info['numeric_precision']; + $column->scale = $info['numeric_scale']; + $column->size = $info['size']; + + if (isset($this->typeMap[$column->dbType])) { + $column->type = $this->typeMap[$column->dbType]; + } else { + $column->type = self::TYPE_STRING; + } + $column->phpType = $this->getColumnPhpType($column); + + return $column; + } } diff --git a/framework/db/sqlite/QueryBuilder.php b/framework/db/sqlite/QueryBuilder.php index bf9bece34b9..9a74dfad880 100644 --- a/framework/db/sqlite/QueryBuilder.php +++ b/framework/db/sqlite/QueryBuilder.php @@ -19,257 +19,258 @@ */ class QueryBuilder extends \yii\db\QueryBuilder { - /** - * @var array mapping from abstract column types (keys) to physical column types (values). - */ - public $typeMap = [ - Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', - Schema::TYPE_STRING => 'varchar(255)', - Schema::TYPE_TEXT => 'text', - Schema::TYPE_SMALLINT => 'smallint', - Schema::TYPE_INTEGER => 'integer', - Schema::TYPE_BIGINT => 'bigint', - Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal(10,0)', - Schema::TYPE_DATETIME => 'datetime', - Schema::TYPE_TIMESTAMP => 'timestamp', - Schema::TYPE_TIME => 'time', - Schema::TYPE_DATE => 'date', - Schema::TYPE_BINARY => 'blob', - Schema::TYPE_BOOLEAN => 'boolean', - Schema::TYPE_MONEY => 'decimal(19,4)', - ]; + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'integer', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DECIMAL => 'decimal(10,0)', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'blob', + Schema::TYPE_BOOLEAN => 'boolean', + Schema::TYPE_MONEY => 'decimal(19,4)', + ]; - /** - * Generates a batch INSERT SQL statement. - * For example, - * - * ~~~ - * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ])->execute(); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the table - * @return string the batch INSERT SQL statement - */ - public function batchInsert($table, $columns, $rows) - { - if (($tableSchema = $this->db->getTableSchema($table)) !== null) { - $columnSchemas = $tableSchema->columns; - } else { - $columnSchemas = []; - } + /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ])->execute(); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows) + { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } - foreach ($columns as $i => $name) { - $columns[$i] = $this->db->quoteColumnName($name); - } + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } - $values = []; - foreach ($rows as $row) { - $vs = []; - foreach ($row as $i => $value) { - if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { - $value = $columnSchemas[$columns[$i]]->typecast($value); - } - $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; - } - $values[] = implode(', ', $vs); - } + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } + $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + } + $values[] = implode(', ', $vs); + } - return 'INSERT INTO ' . $this->db->quoteTableName($table) - . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION ALL ', $values); - } + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION ALL ', $values); + } - /** - * Creates a SQL statement for resetting the sequence value of a table's primary key. - * The sequence will be reset such that the primary key of the next new row inserted - * will have the specified value or 1. - * @param string $tableName the name of the table whose primary key sequence will be reset - * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, - * the next new row's primary key will have a value 1. - * @return string the SQL statement for resetting sequence - * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. - */ - public function resetSequence($tableName, $value = null) - { - $db = $this->db; - $table = $db->getTableSchema($tableName); - if ($table !== null && $table->sequenceName !== null) { - if ($value === null) { - $key = reset($table->primaryKey); - $tableName = $db->quoteTableName($tableName); - $value = $db->createCommand("SELECT MAX('$key') FROM $tableName")->queryScalar(); - } else { - $value = (int)$value - 1; - } - try { - $db->createCommand("UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'")->execute(); - } catch (Exception $e) { - // it's possible that sqlite_sequence does not exist - } - } elseif ($table === null) { - throw new InvalidParamException("Table not found: $tableName"); - } else { - throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); - } - } + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param string $tableName the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @return string the SQL statement for resetting sequence + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function resetSequence($tableName, $value = null) + { + $db = $this->db; + $table = $db->getTableSchema($tableName); + if ($table !== null && $table->sequenceName !== null) { + if ($value === null) { + $key = reset($table->primaryKey); + $tableName = $db->quoteTableName($tableName); + $value = $db->createCommand("SELECT MAX('$key') FROM $tableName")->queryScalar(); + } else { + $value = (int) $value - 1; + } + try { + $db->createCommand("UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'")->execute(); + } catch (Exception $e) { + // it's possible that sqlite_sequence does not exist + } + } elseif ($table === null) { + throw new InvalidParamException("Table not found: $tableName"); + } else { + throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); + } + } - /** - * Enables or disables integrity check. - * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Meaningless for SQLite. - * @param string $table the table name. Meaningless for SQLite. - * @return string the SQL statement for checking integrity - * @throws NotSupportedException this is not supported by SQLite - */ - public function checkIntegrity($check = true, $schema = '', $table = '') - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Enables or disables integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Meaningless for SQLite. + * @param string $table the table name. Meaningless for SQLite. + * @return string the SQL statement for checking integrity + * @throws NotSupportedException this is not supported by SQLite + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for truncating a DB table. - * @param string $table the table to be truncated. The name will be properly quoted by the method. - * @return string the SQL statement for truncating a DB table. - */ - public function truncateTable($table) - { - return "DELETE FROM " . $this->db->quoteTableName($table); - } + /** + * Builds a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return string the SQL statement for truncating a DB table. + */ + public function truncateTable($table) + { + return "DELETE FROM " . $this->db->quoteTableName($table); + } - /** - * Builds a SQL statement for dropping an index. - * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping an index. - */ - public function dropIndex($name, $table) - { - return 'DROP INDEX ' . $this->db->quoteTableName($name); - } + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX ' . $this->db->quoteTableName($name); + } - /** - * Builds a SQL statement for dropping a DB column. - * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. - * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping a DB column. - * @throws NotSupportedException this is not supported by SQLite - */ - public function dropColumn($table, $column) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a DB column. + * @throws NotSupportedException this is not supported by SQLite + */ + public function dropColumn($table, $column) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for renaming a column. - * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. - * @param string $oldName the old name of the column. The name will be properly quoted by the method. - * @param string $newName the new name of the column. The name will be properly quoted by the method. - * @return string the SQL statement for renaming a DB column. - * @throws NotSupportedException this is not supported by SQLite - */ - public function renameColumn($table, $oldName, $newName) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $oldName the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + * @throws NotSupportedException this is not supported by SQLite + */ + public function renameColumn($table, $oldName, $newName) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for adding a foreign key constraint to an existing table. - * The method will properly quote the table and column names. - * @param string $name the name of the foreign key constraint. - * @param string $table the table that the foreign key constraint will be added to. - * @param string|array $columns the name of the column to that the constraint will be added on. - * If there are multiple columns, separate them with commas or use an array to represent them. - * @param string $refTable the table that the foreign key references to. - * @param string|array $refColumns the name of the column that the foreign key references to. - * If there are multiple columns, separate them with commas or use an array to represent them. - * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL - * @return string the SQL statement for adding a foreign key constraint to an existing table. - * @throws NotSupportedException this is not supported by SQLite - */ - public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string|array $columns the name of the column to that the constraint will be added on. + * If there are multiple columns, separate them with commas or use an array to represent them. + * @param string $refTable the table that the foreign key references to. + * @param string|array $refColumns the name of the column that the foreign key references to. + * If there are multiple columns, separate them with commas or use an array to represent them. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return string the SQL statement for adding a foreign key constraint to an existing table. + * @throws NotSupportedException this is not supported by SQLite + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for dropping a foreign key constraint. - * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. - * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. - * @return string the SQL statement for dropping a foreign key constraint. - * @throws NotSupportedException this is not supported by SQLite - */ - public function dropForeignKey($name, $table) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a foreign key constraint. + * @throws NotSupportedException this is not supported by SQLite + */ + public function dropForeignKey($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for changing the definition of a column. - * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. - * @param string $column the name of the column to be changed. The name will be properly quoted by the method. - * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract - * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept - * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' - * will become 'varchar(255) not null'. - * @return string the SQL statement for changing the definition of a column. - * @throws NotSupportedException this is not supported by SQLite - */ - public function alterColumn($table, $column, $type) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract + * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept + * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' + * will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + * @throws NotSupportedException this is not supported by SQLite + */ + public function alterColumn($table, $column, $type) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for adding a primary key constraint to an existing table. - * @param string $name the name of the primary key constraint. - * @param string $table the table that the primary key constraint will be added to. - * @param string|array $columns comma separated string or array of columns that the primary key will consist of. - * @return string the SQL statement for adding a primary key constraint to an existing table. - * @throws NotSupportedException this is not supported by SQLite - */ - public function addPrimaryKey($name, $table, $columns) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for adding a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * @return string the SQL statement for adding a primary key constraint to an existing table. + * @throws NotSupportedException this is not supported by SQLite + */ + public function addPrimaryKey($name, $table, $columns) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * Builds a SQL statement for removing a primary key constraint to an existing table. - * @param string $name the name of the primary key constraint to be removed. - * @param string $table the table that the primary key constraint will be removed from. - * @return string the SQL statement for removing a primary key constraint from an existing table. - * @throws NotSupportedException this is not supported by SQLite * - */ - public function dropPrimaryKey($name, $table) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); - } + /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * @return string the SQL statement for removing a primary key constraint from an existing table. + * @throws NotSupportedException this is not supported by SQLite * + */ + public function dropPrimaryKey($name, $table) + { + throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); + } - /** - * @inheritdoc - */ - public function buildLimit($limit, $offset) - { - $sql = ''; - if ($this->hasLimit($limit)) { - $sql = 'LIMIT ' . $limit; - if ($this->hasOffset($offset)) { - $sql .= ' OFFSET ' . $offset; - } - } elseif ($this->hasOffset($offset)) { - // limit is not optional in SQLite - // http://www.sqlite.org/syntaxdiagrams.html#select-stmt - $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1 - } - return $sql; - } + /** + * @inheritdoc + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + if ($this->hasLimit($limit)) { + $sql = 'LIMIT ' . $limit; + if ($this->hasOffset($offset)) { + $sql .= ' OFFSET ' . $offset; + } + } elseif ($this->hasOffset($offset)) { + // limit is not optional in SQLite + // http://www.sqlite.org/syntaxdiagrams.html#select-stmt + $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1 + } + + return $sql; + } } diff --git a/framework/db/sqlite/Schema.php b/framework/db/sqlite/Schema.php index bf1eb4c0875..67e3d541892 100644 --- a/framework/db/sqlite/Schema.php +++ b/framework/db/sqlite/Schema.php @@ -18,230 +18,233 @@ */ class Schema extends \yii\db\Schema { - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - */ - public $typeMap = [ - 'tinyint' => self::TYPE_SMALLINT, - 'bit' => self::TYPE_SMALLINT, - 'boolean' => self::TYPE_BOOLEAN, - 'bool' => self::TYPE_BOOLEAN, - 'smallint' => self::TYPE_SMALLINT, - 'mediumint' => self::TYPE_INTEGER, - 'int' => self::TYPE_INTEGER, - 'integer' => self::TYPE_INTEGER, - 'bigint' => self::TYPE_BIGINT, - 'float' => self::TYPE_FLOAT, - 'double' => self::TYPE_FLOAT, - 'real' => self::TYPE_FLOAT, - 'decimal' => self::TYPE_DECIMAL, - 'numeric' => self::TYPE_DECIMAL, - 'tinytext' => self::TYPE_TEXT, - 'mediumtext' => self::TYPE_TEXT, - 'longtext' => self::TYPE_TEXT, - 'text' => self::TYPE_TEXT, - 'varchar' => self::TYPE_STRING, - 'string' => self::TYPE_STRING, - 'char' => self::TYPE_STRING, - 'datetime' => self::TYPE_DATETIME, - 'year' => self::TYPE_DATE, - 'date' => self::TYPE_DATE, - 'time' => self::TYPE_TIME, - 'timestamp' => self::TYPE_TIMESTAMP, - 'enum' => self::TYPE_STRING, - ]; - - /** - * Quotes a table name for use in a query. - * A simple table name has no schema prefix. - * @param string $name table name - * @return string the properly quoted table name - */ - public function quoteSimpleTableName($name) - { - return strpos($name, "`") !== false ? $name : "`" . $name . "`"; - } - - /** - * Quotes a column name for use in a query. - * A simple column name has no prefix. - * @param string $name column name - * @return string the properly quoted column name - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; - } - - /** - * Creates a query builder for the MySQL database. - * This method may be overridden by child classes to create a DBMS-specific query builder. - * @return QueryBuilder query builder instance - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Returns all table names in the database. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. - * @return array all table names in the database. The names have NO schema name prefix. - */ - protected function findTableNames($schema = '') - { - $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence'"; - return $this->db->createCommand($sql)->queryColumn(); - } - - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function loadTableSchema($name) - { - $table = new TableSchema; - $table->name = $name; - $table->fullName = $name; - - if ($this->findColumns($table)) { - $this->findConstraints($table); - return $table; - } else { - return null; - } - } - - /** - * Collects the table column metadata. - * @param TableSchema $table the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - $sql = "PRAGMA table_info(" . $this->quoteSimpleTableName($table->name) . ')'; - $columns = $this->db->createCommand($sql)->queryAll(); - if (empty($columns)) { - return false; - } - - foreach ($columns as $info) { - $column = $this->loadColumnSchema($info); - $table->columns[$column->name] = $column; - if ($column->isPrimaryKey) { - $table->primaryKey[] = $column->name; - } - } - if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) { - $table->sequenceName = ''; - $table->columns[$table->primaryKey[0]]->autoIncrement = true; - } - - return true; - } - - /** - * Collects the foreign key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findConstraints($table) - { - $sql = "PRAGMA foreign_key_list(" . $this->quoteSimpleTableName($table->name) . ')'; - $keys = $this->db->createCommand($sql)->queryAll(); - foreach ($keys as $key) { - $id = (int)$key['id']; - if (!isset($table->foreignKeys[$id])) { - $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']]; - } else { - // composite FK - $table->foreignKeys[$id][$key['from']] = $key['to']; - } - } - } - - /** - * Returns all unique indexes for the given table. - * Each array element is of the following structure: - * - * ~~~ - * [ - * 'IndexName1' => ['col1' [, ...]], - * 'IndexName2' => ['col2' [, ...]], - * ] - * ~~~ - * - * @param TableSchema $table the table metadata - * @return array all unique indexes for the given table. - */ - public function findUniqueIndexes($table) - { - $sql = "PRAGMA index_list(" . $this->quoteSimpleTableName($table->name) . ')'; - $indexes = $this->db->createCommand($sql)->queryAll(); - $uniqueIndexes = []; - - foreach ($indexes as $index) { - $indexName = $index['name']; - $indexInfo = $this->db->createCommand("PRAGMA index_info(" . $this->quoteValue($index['name']) . ")")->queryAll(); - - if ($index['unique']) { - $uniqueIndexes[$indexName] = []; - foreach ($indexInfo as $row) { - $uniqueIndexes[$indexName][] = $row['name']; - } - } - } - return $uniqueIndexes; - } - - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema; - $column->name = $info['name']; - $column->allowNull = !$info['notnull']; - $column->isPrimaryKey = $info['pk'] != 0; - - $column->dbType = $info['type']; - $column->unsigned = strpos($column->dbType, 'unsigned') !== false; - - $column->type = self::TYPE_STRING; - if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { - $type = strtolower($matches[1]); - if (isset($this->typeMap[$type])) { - $column->type = $this->typeMap[$type]; - } - - if (!empty($matches[2])) { - $values = explode(',', $matches[2]); - $column->size = $column->precision = (int)$values[0]; - if (isset($values[1])) { - $column->scale = (int)$values[1]; - } - if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { - $column->type = 'boolean'; - } elseif ($type === 'bit') { - if ($column->size > 32) { - $column->type = 'bigint'; - } elseif ($column->size === 32) { - $column->type = 'integer'; - } - } - } - } - $column->phpType = $this->getColumnPhpType($column); - - $value = $info['dflt_value']; - if ($column->type === 'string') { - $column->defaultValue = trim($value, "'\""); - } else { - $column->defaultValue = $column->typecast(strcasecmp($value, 'null') ? $value : null); - } - - return $column; - } + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = [ + 'tinyint' => self::TYPE_SMALLINT, + 'bit' => self::TYPE_SMALLINT, + 'boolean' => self::TYPE_BOOLEAN, + 'bool' => self::TYPE_BOOLEAN, + 'smallint' => self::TYPE_SMALLINT, + 'mediumint' => self::TYPE_INTEGER, + 'int' => self::TYPE_INTEGER, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'float' => self::TYPE_FLOAT, + 'double' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + 'decimal' => self::TYPE_DECIMAL, + 'numeric' => self::TYPE_DECIMAL, + 'tinytext' => self::TYPE_TEXT, + 'mediumtext' => self::TYPE_TEXT, + 'longtext' => self::TYPE_TEXT, + 'text' => self::TYPE_TEXT, + 'varchar' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + 'char' => self::TYPE_STRING, + 'datetime' => self::TYPE_DATETIME, + 'year' => self::TYPE_DATE, + 'date' => self::TYPE_DATE, + 'time' => self::TYPE_TIME, + 'timestamp' => self::TYPE_TIMESTAMP, + 'enum' => self::TYPE_STRING, + ]; + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, "`") !== false ? $name : "`" . $name . "`"; + } + + /** + * Quotes a column name for use in a query. + * A simple column name has no prefix. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`'; + } + + /** + * Creates a query builder for the MySQL database. + * This method may be overridden by child classes to create a DBMS-specific query builder. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array all table names in the database. The names have NO schema name prefix. + */ + protected function findTableNames($schema = '') + { + $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence'"; + + return $this->db->createCommand($sql)->queryColumn(); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTableSchema($name) + { + $table = new TableSchema; + $table->name = $name; + $table->fullName = $name; + + if ($this->findColumns($table)) { + $this->findConstraints($table); + + return $table; + } else { + return null; + } + } + + /** + * Collects the table column metadata. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $sql = "PRAGMA table_info(" . $this->quoteSimpleTableName($table->name) . ')'; + $columns = $this->db->createCommand($sql)->queryAll(); + if (empty($columns)) { + return false; + } + + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey) { + $table->primaryKey[] = $column->name; + } + } + if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) { + $table->sequenceName = ''; + $table->columns[$table->primaryKey[0]]->autoIncrement = true; + } + + return true; + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $sql = "PRAGMA foreign_key_list(" . $this->quoteSimpleTableName($table->name) . ')'; + $keys = $this->db->createCommand($sql)->queryAll(); + foreach ($keys as $key) { + $id = (int) $key['id']; + if (!isset($table->foreignKeys[$id])) { + $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']]; + } else { + // composite FK + $table->foreignKeys[$id][$key['from']] = $key['to']; + } + } + } + + /** + * Returns all unique indexes for the given table. + * Each array element is of the following structure: + * + * ~~~ + * [ + * 'IndexName1' => ['col1' [, ...]], + * 'IndexName2' => ['col2' [, ...]], + * ] + * ~~~ + * + * @param TableSchema $table the table metadata + * @return array all unique indexes for the given table. + */ + public function findUniqueIndexes($table) + { + $sql = "PRAGMA index_list(" . $this->quoteSimpleTableName($table->name) . ')'; + $indexes = $this->db->createCommand($sql)->queryAll(); + $uniqueIndexes = []; + + foreach ($indexes as $index) { + $indexName = $index['name']; + $indexInfo = $this->db->createCommand("PRAGMA index_info(" . $this->quoteValue($index['name']) . ")")->queryAll(); + + if ($index['unique']) { + $uniqueIndexes[$indexName] = []; + foreach ($indexInfo as $row) { + $uniqueIndexes[$indexName][] = $row['name']; + } + } + } + + return $uniqueIndexes; + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema; + $column->name = $info['name']; + $column->allowNull = !$info['notnull']; + $column->isPrimaryKey = $info['pk'] != 0; + + $column->dbType = $info['type']; + $column->unsigned = strpos($column->dbType, 'unsigned') !== false; + + $column->type = self::TYPE_STRING; + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = strtolower($matches[1]); + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + + if (!empty($matches[2])) { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int) $values[0]; + if (isset($values[1])) { + $column->scale = (int) $values[1]; + } + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + $column->phpType = $this->getColumnPhpType($column); + + $value = $info['dflt_value']; + if ($column->type === 'string') { + $column->defaultValue = trim($value, "'\""); + } else { + $column->defaultValue = $column->typecast(strcasecmp($value, 'null') ? $value : null); + } + + return $column; + } } diff --git a/framework/grid/ActionColumn.php b/framework/grid/ActionColumn.php index dbabc1c7982..af8f25c172f 100644 --- a/framework/grid/ActionColumn.php +++ b/framework/grid/ActionColumn.php @@ -32,120 +32,121 @@ */ class ActionColumn extends Column { - /** - * @var string the ID of the controller that should handle the actions specified here. - * If not set, it will use the currently active controller. This property is mainly used by - * [[urlCreator]] to create URLs for different actions. The value of this property will be prefixed - * to each action name to form the route of the action. - */ - public $controller; - /** - * @var string the template used for composing each cell in the action column. - * Tokens enclosed within curly brackets are treated as controller action IDs (also called *button names* - * in the context of action column). They will be replaced by the corresponding button rendering callbacks - * specified in [[buttons]]. For example, the token `{view}` will be replaced by the result of - * the callback `buttons['view']`. If a callback cannot be found, the token will be replaced with an empty string. - * @see buttons - */ - public $template = '{view} {update} {delete}'; - /** - * @var array button rendering callbacks. The array keys are the button names (without curly brackets), - * and the values are the corresponding button rendering callbacks. The callbacks should use the following - * signature: - * - * ```php - * function ($url, $model) { - * // return the button HTML code - * } - * ``` - * - * where `$url` is the URL that the column creates for the button, and `$model` is the model object - * being rendered for the current row. - */ - public $buttons = []; - /** - * @var callable a callback that creates a button URL using the specified model information. - * The signature of the callback should be the same as that of [[createUrl()]]. - * If this property is not set, button URLs will be created using [[createUrl()]]. - */ - public $urlCreator; + /** + * @var string the ID of the controller that should handle the actions specified here. + * If not set, it will use the currently active controller. This property is mainly used by + * [[urlCreator]] to create URLs for different actions. The value of this property will be prefixed + * to each action name to form the route of the action. + */ + public $controller; + /** + * @var string the template used for composing each cell in the action column. + * Tokens enclosed within curly brackets are treated as controller action IDs (also called *button names* + * in the context of action column). They will be replaced by the corresponding button rendering callbacks + * specified in [[buttons]]. For example, the token `{view}` will be replaced by the result of + * the callback `buttons['view']`. If a callback cannot be found, the token will be replaced with an empty string. + * @see buttons + */ + public $template = '{view} {update} {delete}'; + /** + * @var array button rendering callbacks. The array keys are the button names (without curly brackets), + * and the values are the corresponding button rendering callbacks. The callbacks should use the following + * signature: + * + * ```php + * function ($url, $model) { + * // return the button HTML code + * } + * ``` + * + * where `$url` is the URL that the column creates for the button, and `$model` is the model object + * being rendered for the current row. + */ + public $buttons = []; + /** + * @var callable a callback that creates a button URL using the specified model information. + * The signature of the callback should be the same as that of [[createUrl()]]. + * If this property is not set, button URLs will be created using [[createUrl()]]. + */ + public $urlCreator; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->initDefaultButtons(); + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - $this->initDefaultButtons(); - } + /** + * Initializes the default button rendering callbacks + */ + protected function initDefaultButtons() + { + if (!isset($this->buttons['view'])) { + $this->buttons['view'] = function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('yii', 'View'), + 'data-pjax' => '0', + ]); + }; + } + if (!isset($this->buttons['update'])) { + $this->buttons['update'] = function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('yii', 'Update'), + 'data-pjax' => '0', + ]); + }; + } + if (!isset($this->buttons['delete'])) { + $this->buttons['delete'] = function ($url, $model) { + return Html::a('', $url, [ + 'title' => Yii::t('yii', 'Delete'), + 'data-confirm' => Yii::t('yii', 'Are you sure you want to delete this item?'), + 'data-method' => 'post', + 'data-pjax' => '0', + ]); + }; + } + } - /** - * Initializes the default button rendering callbacks - */ - protected function initDefaultButtons() - { - if (!isset($this->buttons['view'])) { - $this->buttons['view'] = function ($url, $model) { - return Html::a('', $url, [ - 'title' => Yii::t('yii', 'View'), - 'data-pjax' => '0', - ]); - }; - } - if (!isset($this->buttons['update'])) { - $this->buttons['update'] = function ($url, $model) { - return Html::a('', $url, [ - 'title' => Yii::t('yii', 'Update'), - 'data-pjax' => '0', - ]); - }; - } - if (!isset($this->buttons['delete'])) { - $this->buttons['delete'] = function ($url, $model) { - return Html::a('', $url, [ - 'title' => Yii::t('yii', 'Delete'), - 'data-confirm' => Yii::t('yii', 'Are you sure you want to delete this item?'), - 'data-method' => 'post', - 'data-pjax' => '0', - ]); - }; - } - } + /** + * Creates a URL for the given action and model. + * This method is called for each button and each row. + * @param string $action the button name (or action ID) + * @param \yii\db\ActiveRecord $model the data model + * @param mixed $key the key associated with the data model + * @param integer $index the current row index + * @return string the created URL + */ + public function createUrl($action, $model, $key, $index) + { + if ($this->urlCreator instanceof Closure) { + return call_user_func($this->urlCreator, $action, $model, $key, $index); + } else { + $params = is_array($key) ? $key : ['id' => (string) $key]; + $params[0] = $this->controller ? $this->controller . '/' . $action : $action; - /** - * Creates a URL for the given action and model. - * This method is called for each button and each row. - * @param string $action the button name (or action ID) - * @param \yii\db\ActiveRecord $model the data model - * @param mixed $key the key associated with the data model - * @param integer $index the current row index - * @return string the created URL - */ - public function createUrl($action, $model, $key, $index) - { - if ($this->urlCreator instanceof Closure) { - return call_user_func($this->urlCreator, $action, $model, $key, $index); - } else { - $params = is_array($key) ? $key : ['id' => (string)$key]; - $params[0] = $this->controller ? $this->controller . '/' . $action : $action; - return Url::toRoute($params); - } - } + return Url::toRoute($params); + } + } - /** - * @inheritdoc - */ - protected function renderDataCellContent($model, $key, $index) - { - return preg_replace_callback('/\\{([\w\-\/]+)\\}/', function ($matches) use ($model, $key, $index) { - $name = $matches[1]; - if (isset($this->buttons[$name])) { - $url = $this->createUrl($name, $model, $key, $index); - return call_user_func($this->buttons[$name], $url, $model); - } else { - return ''; - } - }, $this->template); - } + /** + * @inheritdoc + */ + protected function renderDataCellContent($model, $key, $index) + { + return preg_replace_callback('/\\{([\w\-\/]+)\\}/', function ($matches) use ($model, $key, $index) { + $name = $matches[1]; + if (isset($this->buttons[$name])) { + $url = $this->createUrl($name, $model, $key, $index); + + return call_user_func($this->buttons[$name], $url, $model); + } else { + return ''; + } + }, $this->template); + } } diff --git a/framework/grid/CheckboxColumn.php b/framework/grid/CheckboxColumn.php index 152242544a7..ddb30dbf1c7 100644 --- a/framework/grid/CheckboxColumn.php +++ b/framework/grid/CheckboxColumn.php @@ -39,73 +39,73 @@ */ class CheckboxColumn extends Column { - /** - * @var string the name of the input checkbox input fields. This will be appended with `[]` to ensure it is an array. - */ - public $name = 'selection'; - /** - * @var array HTML attributes for the checkboxes. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $checkboxOptions = []; - /** - * @var bool whether it is possible to select multiple rows. Defaults to `true`. - */ - public $multiple = true; + /** + * @var string the name of the input checkbox input fields. This will be appended with `[]` to ensure it is an array. + */ + public $name = 'selection'; + /** + * @var array HTML attributes for the checkboxes. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $checkboxOptions = []; + /** + * @var bool whether it is possible to select multiple rows. Defaults to `true`. + */ + public $multiple = true; + /** + * @inheritdoc + * @throws \yii\base\InvalidConfigException if [[name]] is not set. + */ + public function init() + { + parent::init(); + if (empty($this->name)) { + throw new InvalidConfigException('The "name" property must be set.'); + } + if (substr($this->name, -2) !== '[]') { + $this->name .= '[]'; + } + } - /** - * @inheritdoc - * @throws \yii\base\InvalidConfigException if [[name]] is not set. - */ - public function init() - { - parent::init(); - if (empty($this->name)) { - throw new InvalidConfigException('The "name" property must be set.'); - } - if (substr($this->name, -2) !== '[]') { - $this->name .= '[]'; - } - } + /** + * Renders the header cell content. + * The default implementation simply renders [[header]]. + * This method may be overridden to customize the rendering of the header cell. + * @return string the rendering result + */ + protected function renderHeaderCellContent() + { + $name = rtrim($this->name, '[]') . '_all'; + $id = $this->grid->options['id']; + $options = json_encode([ + 'name' => $this->name, + 'multiple' => $this->multiple, + 'checkAll' => $name, + ]); + $this->grid->getView()->registerJs("jQuery('#$id').yiiGridView('setSelectionColumn', $options);"); - /** - * Renders the header cell content. - * The default implementation simply renders [[header]]. - * This method may be overridden to customize the rendering of the header cell. - * @return string the rendering result - */ - protected function renderHeaderCellContent() - { - $name = rtrim($this->name, '[]') . '_all'; - $id = $this->grid->options['id']; - $options = json_encode([ - 'name' => $this->name, - 'multiple' => $this->multiple, - 'checkAll' => $name, - ]); - $this->grid->getView()->registerJs("jQuery('#$id').yiiGridView('setSelectionColumn', $options);"); + if ($this->header !== null || !$this->multiple) { + return parent::renderHeaderCellContent(); + } else { + return Html::checkBox($name, false, ['class' => 'select-on-check-all']); + } + } - if ($this->header !== null || !$this->multiple) { - return parent::renderHeaderCellContent(); - } else { - return Html::checkBox($name, false, ['class' => 'select-on-check-all']); - } - } + /** + * @inheritdoc + */ + protected function renderDataCellContent($model, $key, $index) + { + if ($this->checkboxOptions instanceof Closure) { + $options = call_user_func($this->checkboxOptions, $model, $key, $index, $this); + } else { + $options = $this->checkboxOptions; + if (!isset($options['value'])) { + $options['value'] = is_array($key) ? json_encode($key) : $key; + } + } - /** - * @inheritdoc - */ - protected function renderDataCellContent($model, $key, $index) - { - if ($this->checkboxOptions instanceof Closure) { - $options = call_user_func($this->checkboxOptions, $model, $key, $index, $this); - } else { - $options = $this->checkboxOptions; - if (!isset($options['value'])) { - $options['value'] = is_array($key) ? json_encode($key) : $key; - } - } - return Html::checkbox($this->name, !empty($options['checked']), $options); - } + return Html::checkbox($this->name, !empty($options['checked']), $options); + } } diff --git a/framework/grid/Column.php b/framework/grid/Column.php index 23eaf513980..4da20f42670 100644 --- a/framework/grid/Column.php +++ b/framework/grid/Column.php @@ -19,157 +19,157 @@ */ class Column extends Object { - /** - * @var GridView the grid view object that owns this column. - */ - public $grid; - /** - * @var string the header cell content. Note that it will not be HTML-encoded. - */ - public $header; - /** - * @var string the footer cell content. Note that it will not be HTML-encoded. - */ - public $footer; - /** - * @var callable - */ - public $content; - /** - * @var boolean whether this column is visible. Defaults to true. - */ - public $visible = true; - /** - * @var array the HTML attributes for the column group tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array the HTML attributes for the header cell tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $headerOptions = []; - /** - * @var array|\Closure the HTML attributes for the data cell tag. This can either be an array of - * attributes or an anonymous function that ([[Closure]]) that returns such an array. - * The signature of the function should be the following: `function ($model, $key, $index, $gridView)`. - * A function may be used to assign different attributes to different rows based on the data in that row. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $contentOptions = []; - /** - * @var array the HTML attributes for the footer cell tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $footerOptions = []; - /** - * @var array the HTML attributes for the filter cell tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $filterOptions = []; + /** + * @var GridView the grid view object that owns this column. + */ + public $grid; + /** + * @var string the header cell content. Note that it will not be HTML-encoded. + */ + public $header; + /** + * @var string the footer cell content. Note that it will not be HTML-encoded. + */ + public $footer; + /** + * @var callable + */ + public $content; + /** + * @var boolean whether this column is visible. Defaults to true. + */ + public $visible = true; + /** + * @var array the HTML attributes for the column group tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the HTML attributes for the header cell tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array|\Closure the HTML attributes for the data cell tag. This can either be an array of + * attributes or an anonymous function that ([[Closure]]) that returns such an array. + * The signature of the function should be the following: `function ($model, $key, $index, $gridView)`. + * A function may be used to assign different attributes to different rows based on the data in that row. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $contentOptions = []; + /** + * @var array the HTML attributes for the footer cell tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $footerOptions = []; + /** + * @var array the HTML attributes for the filter cell tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $filterOptions = []; + /** + * Renders the header cell. + */ + public function renderHeaderCell() + { + return Html::tag('th', $this->renderHeaderCellContent(), $this->headerOptions); + } - /** - * Renders the header cell. - */ - public function renderHeaderCell() - { - return Html::tag('th', $this->renderHeaderCellContent(), $this->headerOptions); - } + /** + * Renders the footer cell. + */ + public function renderFooterCell() + { + return Html::tag('td', $this->renderFooterCellContent(), $this->footerOptions); + } - /** - * Renders the footer cell. - */ - public function renderFooterCell() - { - return Html::tag('td', $this->renderFooterCellContent(), $this->footerOptions); - } + /** + * Renders a data cell. + * @param mixed $model the data model being rendered + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data item among the item array returned by [[GridView::dataProvider]]. + * @return string the rendering result + */ + public function renderDataCell($model, $key, $index) + { + if ($this->contentOptions instanceof Closure) { + $options = call_user_func($this->contentOptions, $model, $key, $index, $this); + } else { + $options = $this->contentOptions; + } - /** - * Renders a data cell. - * @param mixed $model the data model being rendered - * @param mixed $key the key associated with the data model - * @param integer $index the zero-based index of the data item among the item array returned by [[GridView::dataProvider]]. - * @return string the rendering result - */ - public function renderDataCell($model, $key, $index) - { - if ($this->contentOptions instanceof Closure) { - $options = call_user_func($this->contentOptions, $model, $key, $index, $this); - } else { - $options = $this->contentOptions; - } - return Html::tag('td', $this->renderDataCellContent($model, $key, $index), $options); - } + return Html::tag('td', $this->renderDataCellContent($model, $key, $index), $options); + } - /** - * Renders the filter cell. - */ - public function renderFilterCell() - { - return Html::tag('td', $this->renderFilterCellContent(), $this->filterOptions); - } + /** + * Renders the filter cell. + */ + public function renderFilterCell() + { + return Html::tag('td', $this->renderFilterCellContent(), $this->filterOptions); + } - /** - * Renders the header cell content. - * The default implementation simply renders [[header]]. - * This method may be overridden to customize the rendering of the header cell. - * @return string the rendering result - */ - protected function renderHeaderCellContent() - { - return trim($this->header) !== '' ? $this->header : $this->grid->emptyCell; - } + /** + * Renders the header cell content. + * The default implementation simply renders [[header]]. + * This method may be overridden to customize the rendering of the header cell. + * @return string the rendering result + */ + protected function renderHeaderCellContent() + { + return trim($this->header) !== '' ? $this->header : $this->grid->emptyCell; + } - /** - * Renders the footer cell content. - * The default implementation simply renders [[footer]]. - * This method may be overridden to customize the rendering of the footer cell. - * @return string the rendering result - */ - protected function renderFooterCellContent() - { - return trim($this->footer) !== '' ? $this->footer : $this->grid->emptyCell; - } + /** + * Renders the footer cell content. + * The default implementation simply renders [[footer]]. + * This method may be overridden to customize the rendering of the footer cell. + * @return string the rendering result + */ + protected function renderFooterCellContent() + { + return trim($this->footer) !== '' ? $this->footer : $this->grid->emptyCell; + } - /** - * Returns the raw data cell content. - * This method is called by [[renderDataCellContent()]] when rendering the content of a data cell. - * @param mixed $model the data model - * @param mixed $key the key associated with the data model - * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. - * @return string the rendering result - */ - protected function getDataCellContent($model, $key, $index) - { - if ($this->content !== null) { - return call_user_func($this->content, $model, $key, $index, $this); - } else { - return null; - } - } + /** + * Returns the raw data cell content. + * This method is called by [[renderDataCellContent()]] when rendering the content of a data cell. + * @param mixed $model the data model + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. + * @return string the rendering result + */ + protected function getDataCellContent($model, $key, $index) + { + if ($this->content !== null) { + return call_user_func($this->content, $model, $key, $index, $this); + } else { + return null; + } + } - /** - * Renders the data cell content. - * @param mixed $model the data model - * @param mixed $key the key associated with the data model - * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. - * @return string the rendering result - */ - protected function renderDataCellContent($model, $key, $index) - { - return $this->content !== null ? $this->getDataCellContent($model, $key, $index) : $this->grid->emptyCell; - } + /** + * Renders the data cell content. + * @param mixed $model the data model + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. + * @return string the rendering result + */ + protected function renderDataCellContent($model, $key, $index) + { + return $this->content !== null ? $this->getDataCellContent($model, $key, $index) : $this->grid->emptyCell; + } - /** - * Renders the filter cell content. - * The default implementation simply renders a space. - * This method may be overridden to customize the rendering of the filter cell (if any). - * @return string the rendering result - */ - protected function renderFilterCellContent() - { - return $this->grid->emptyCell; - } + /** + * Renders the filter cell content. + * The default implementation simply renders a space. + * This method may be overridden to customize the rendering of the filter cell (if any). + * @return string the rendering result + */ + protected function renderFilterCellContent() + { + return $this->grid->emptyCell; + } } diff --git a/framework/grid/DataColumn.php b/framework/grid/DataColumn.php index 44cf2cb6d2c..2facd2a4686 100644 --- a/framework/grid/DataColumn.php +++ b/framework/grid/DataColumn.php @@ -24,149 +24,150 @@ */ class DataColumn extends Column { - /** - * @var string the attribute name associated with this column. When neither [[content]] nor [[value]] - * is specified, the value of the specified attribute will be retrieved from each data model and displayed. - * - * Also, if [[label]] is not specified, the label associated with the attribute will be displayed. - */ - public $attribute; - /** - * @var string label to be displayed in the [[header|header cell]] and also to be used as the sorting - * link label when sorting is enabled for this column. - * If it is not set and the models provided by the GridViews data provider are instances - * of [[\yii\db\ActiveRecord]], the label will be determined using [[\yii\db\ActiveRecord::getAttributeLabel()]]. - * Otherwise [[\yii\helpers\Inflector::camel2words()]] will be used to get a label. - */ - public $label; - /** - * @var string|\Closure the attribute name to be displayed in this column or an anonymous function that returns - * the value to be displayed for every data model. - * The signature of this function is `function ($model, $index, $widget)`. - * If this is not set, `$model[$attribute]` will be used to obtain the value. - */ - public $value; - /** - * @var string|array in which format should the value of each data model be displayed as (e.g. "raw", "text", "html", - * ['date', 'Y-m-d']). Supported formats are determined by the [[GridView::formatter|formatter]] used by - * the [[GridView]]. Default format is "text" which will format the value as an HTML-encoded plain text when - * [[\yii\base\Formatter::format()]] or [[\yii\i18n\Formatter::format()]] is used. - */ - public $format = 'text'; - /** - * @var boolean whether to allow sorting by this column. If true and [[attribute]] is found in - * the sort definition of [[GridView::dataProvider]], then the header cell of this column - * will contain a link that may trigger the sorting when being clicked. - */ - public $enableSorting = true; - /** - * @var array the HTML attributes for the link tag in the header cell - * generated by [[\yii\data\Sort::link]] when sorting is enabled for this column. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $sortLinkOptions = []; - /** - * @var string|array|boolean the HTML code representing a filter input (e.g. a text field, a dropdown list) - * that is used for this data column. This property is effective only when [[GridView::filterModel]] is set. - * - * - If this property is not set, a text field will be generated as the filter input; - * - If this property is an array, a dropdown list will be generated that uses this property value as - * the list options. - * - If you don't want a filter for this data column, set this value to be false. - */ - public $filter; - /** - * @var array the HTML attributes for the filter input fields. This property is used in combination with - * the [[filter]] property. When [[filter]] is not set or is an array, this property will be used to - * render the HTML attributes for the generated filter input fields. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $filterInputOptions = ['class' => 'form-control', 'id' => null]; + /** + * @var string the attribute name associated with this column. When neither [[content]] nor [[value]] + * is specified, the value of the specified attribute will be retrieved from each data model and displayed. + * + * Also, if [[label]] is not specified, the label associated with the attribute will be displayed. + */ + public $attribute; + /** + * @var string label to be displayed in the [[header|header cell]] and also to be used as the sorting + * link label when sorting is enabled for this column. + * If it is not set and the models provided by the GridViews data provider are instances + * of [[\yii\db\ActiveRecord]], the label will be determined using [[\yii\db\ActiveRecord::getAttributeLabel()]]. + * Otherwise [[\yii\helpers\Inflector::camel2words()]] will be used to get a label. + */ + public $label; + /** + * @var string|\Closure the attribute name to be displayed in this column or an anonymous function that returns + * the value to be displayed for every data model. + * The signature of this function is `function ($model, $index, $widget)`. + * If this is not set, `$model[$attribute]` will be used to obtain the value. + */ + public $value; + /** + * @var string|array in which format should the value of each data model be displayed as (e.g. "raw", "text", "html", + * ['date', 'Y-m-d']). Supported formats are determined by the [[GridView::formatter|formatter]] used by + * the [[GridView]]. Default format is "text" which will format the value as an HTML-encoded plain text when + * [[\yii\base\Formatter::format()]] or [[\yii\i18n\Formatter::format()]] is used. + */ + public $format = 'text'; + /** + * @var boolean whether to allow sorting by this column. If true and [[attribute]] is found in + * the sort definition of [[GridView::dataProvider]], then the header cell of this column + * will contain a link that may trigger the sorting when being clicked. + */ + public $enableSorting = true; + /** + * @var array the HTML attributes for the link tag in the header cell + * generated by [[\yii\data\Sort::link]] when sorting is enabled for this column. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $sortLinkOptions = []; + /** + * @var string|array|boolean the HTML code representing a filter input (e.g. a text field, a dropdown list) + * that is used for this data column. This property is effective only when [[GridView::filterModel]] is set. + * + * - If this property is not set, a text field will be generated as the filter input; + * - If this property is an array, a dropdown list will be generated that uses this property value as + * the list options. + * - If you don't want a filter for this data column, set this value to be false. + */ + public $filter; + /** + * @var array the HTML attributes for the filter input fields. This property is used in combination with + * the [[filter]] property. When [[filter]] is not set or is an array, this property will be used to + * render the HTML attributes for the generated filter input fields. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $filterInputOptions = ['class' => 'form-control', 'id' => null]; - /** - * @inheritdoc - */ - protected function renderHeaderCellContent() - { - if ($this->header !== null || $this->label === null && $this->attribute === null) { - return parent::renderHeaderCellContent(); - } + /** + * @inheritdoc + */ + protected function renderHeaderCellContent() + { + if ($this->header !== null || $this->label === null && $this->attribute === null) { + return parent::renderHeaderCellContent(); + } - $provider = $this->grid->dataProvider; + $provider = $this->grid->dataProvider; - if ($this->label === null) { - if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQueryInterface) { - /** @var Model $model */ - $model = new $provider->query->modelClass; - $label = $model->getAttributeLabel($this->attribute); - } else { - $models = $provider->getModels(); - if (($model = reset($models)) instanceof Model) { - /** @var Model $model */ - $label = $model->getAttributeLabel($this->attribute); - } else { - $label = Inflector::camel2words($this->attribute); - } - } - } else { - $label = $this->label; - } + if ($this->label === null) { + if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQueryInterface) { + /** @var Model $model */ + $model = new $provider->query->modelClass; + $label = $model->getAttributeLabel($this->attribute); + } else { + $models = $provider->getModels(); + if (($model = reset($models)) instanceof Model) { + /** @var Model $model */ + $label = $model->getAttributeLabel($this->attribute); + } else { + $label = Inflector::camel2words($this->attribute); + } + } + } else { + $label = $this->label; + } - if ($this->attribute !== null && $this->enableSorting && - ($sort = $provider->getSort()) !== false && $sort->hasAttribute($this->attribute)) { + if ($this->attribute !== null && $this->enableSorting && + ($sort = $provider->getSort()) !== false && $sort->hasAttribute($this->attribute)) { + return $sort->link($this->attribute, array_merge($this->sortLinkOptions, ['label' => Html::encode($label)])); + } else { + return Html::encode($label); + } + } - return $sort->link($this->attribute, array_merge($this->sortLinkOptions, ['label' => Html::encode($label)])); - } else { - return Html::encode($label); - } - } + /** + * @inheritdoc + */ + protected function renderFilterCellContent() + { + if (is_string($this->filter)) { + return $this->filter; + } elseif ($this->filter !== false && $this->grid->filterModel instanceof Model && + $this->attribute !== null && $this->grid->filterModel->isAttributeActive($this->attribute)) + { + if (is_array($this->filter)) { + $options = array_merge(['prompt' => ''], $this->filterInputOptions); - /** - * @inheritdoc - */ - protected function renderFilterCellContent() - { - if (is_string($this->filter)) { - return $this->filter; - } elseif ($this->filter !== false && $this->grid->filterModel instanceof Model && - $this->attribute !== null && $this->grid->filterModel->isAttributeActive($this->attribute)) - { - if (is_array($this->filter)) { - $options = array_merge(['prompt' => ''], $this->filterInputOptions); - return Html::activeDropDownList($this->grid->filterModel, $this->attribute, $this->filter, $options); - } else { - return Html::activeTextInput($this->grid->filterModel, $this->attribute, $this->filterInputOptions); - } - } else { - return parent::renderFilterCellContent(); - } - } + return Html::activeDropDownList($this->grid->filterModel, $this->attribute, $this->filter, $options); + } else { + return Html::activeTextInput($this->grid->filterModel, $this->attribute, $this->filterInputOptions); + } + } else { + return parent::renderFilterCellContent(); + } + } - /** - * @inheritdoc - */ - protected function getDataCellContent($model, $key, $index) - { - if ($this->value !== null) { - if (is_string($this->value)) { - $value = ArrayHelper::getValue($model, $this->value); - } else { - $value = call_user_func($this->value, $model, $index, $this); - } - } elseif ($this->content === null && $this->attribute !== null) { - $value = ArrayHelper::getValue($model, $this->attribute); - } else { - return parent::getDataCellContent($model, $key, $index); - } - return $value; - } + /** + * @inheritdoc + */ + protected function getDataCellContent($model, $key, $index) + { + if ($this->value !== null) { + if (is_string($this->value)) { + $value = ArrayHelper::getValue($model, $this->value); + } else { + $value = call_user_func($this->value, $model, $index, $this); + } + } elseif ($this->content === null && $this->attribute !== null) { + $value = ArrayHelper::getValue($model, $this->attribute); + } else { + return parent::getDataCellContent($model, $key, $index); + } - /** - * @inheritdoc - */ - protected function renderDataCellContent($model, $key, $index) - { - return $this->grid->formatter->format($this->getDataCellContent($model, $key, $index), $this->format); - } + return $value; + } + + /** + * @inheritdoc + */ + protected function renderDataCellContent($model, $key, $index) + { + return $this->grid->formatter->format($this->getDataCellContent($model, $key, $index), $this->format); + } } diff --git a/framework/grid/GridView.php b/framework/grid/GridView.php index e287f8f6f7a..00964926022 100644 --- a/framework/grid/GridView.php +++ b/framework/grid/GridView.php @@ -26,440 +26,447 @@ */ class GridView extends BaseListView { - const FILTER_POS_HEADER = 'header'; - const FILTER_POS_FOOTER = 'footer'; - const FILTER_POS_BODY = 'body'; - - /** - * @var string the default data column class if the class name is not explicitly specified when configuring a data column. - * Defaults to 'yii\grid\DataColumn'. - */ - public $dataColumnClass; - /** - * @var string the caption of the grid table - * @see captionOptions - */ - public $caption; - /** - * @var array the HTML attributes for the caption element. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - * @see caption - */ - public $captionOptions = []; - /** - * @var array the HTML attributes for the grid table element. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $tableOptions = ['class' => 'table table-striped table-bordered']; - /** - * @var array the HTML attributes for the container tag of the grid view. - * The "tag" element specifies the tag name of the container element and defaults to "div". - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'grid-view']; - /** - * @var array the HTML attributes for the table header row. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $headerRowOptions = []; - /** - * @var array the HTML attributes for the table footer row. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $footerRowOptions = []; - /** - * @var array|Closure the HTML attributes for the table body rows. This can be either an array - * specifying the common HTML attributes for all body rows, or an anonymous function that - * returns an array of the HTML attributes. The anonymous function will be called once for every - * data model returned by [[dataProvider]]. It should have the following signature: - * - * ```php - * function ($model, $key, $index, $grid) - * ``` - * - * - `$model`: the current data model being rendered - * - `$key`: the key value associated with the current data model - * - `$index`: the zero-based index of the data model in the model array returned by [[dataProvider]] - * - `$grid`: the GridView object - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $rowOptions = []; - /** - * @var Closure an anonymous function that is called once BEFORE rendering each data model. - * It should have the similar signature as [[rowOptions]]. The return result of the function - * will be rendered directly. - */ - public $beforeRow; - /** - * @var Closure an anonymous function that is called once AFTER rendering each data model. - * It should have the similar signature as [[rowOptions]]. The return result of the function - * will be rendered directly. - */ - public $afterRow; - /** - * @var boolean whether to show the header section of the grid table. - */ - public $showHeader = true; - /** - * @var boolean whether to show the footer section of the grid table. - */ - public $showFooter = false; - /** - * @var boolean whether to show the grid view if [[dataProvider]] returns no data. - */ - public $showOnEmpty = true; - /** - * @var array|Formatter the formatter used to format model attribute values into displayable texts. - * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] - * instance. If this property is not set, the "formatter" application component will be used. - */ - public $formatter; - /** - * @var array grid column configuration. Each array element represents the configuration - * for one particular grid column. For example, - * - * ```php - * [ - * ['class' => SerialColumn::className()], - * [ - * 'class' => DataColumn::className(), - * 'attribute' => 'name', - * 'format' => 'text', - * 'label' => 'Name', - * ], - * ['class' => CheckboxColumn::className()], - * ] - * ``` - * - * If a column is of class [[DataColumn]], the "class" element can be omitted. - * - * As a shortcut format, a string may be used to specify the configuration of a data column - * which only contains "attribute", "format", and/or "label" options: `"attribute:format:label"`. - * For example, the above "name" column can also be specified as: `"name:text:Name"`. - * Both "format" and "label" are optional. They will take default values if absent. - */ - public $columns = []; - public $emptyCell = ' '; - /** - * @var \yii\base\Model the model that keeps the user-entered filter data. When this property is set, - * the grid view will enable column-based filtering. Each data column by default will display a text field - * at the top that users can fill in to filter the data. - * - * Note that in order to show an input field for filtering, a column must have its [[DataColumn::attribute]] - * property set or have [[DataColumn::filter]] set as the HTML code for the input field. - * - * When this property is not set (null) the filtering feature is disabled. - */ - public $filterModel; - /** - * @var string|array the URL for returning the filtering result. [[Url::to()]] will be called to - * normalize the URL. If not set, the current controller action will be used. - * When the user makes change to any filter input, the current filtering inputs will be appended - * as GET parameters to this URL. - */ - public $filterUrl; - public $filterSelector; - /** - * @var string whether the filters should be displayed in the grid view. Valid values include: - * - * - [[FILTER_POS_HEADER]]: the filters will be displayed on top of each column's header cell. - * - [[FILTER_POS_BODY]]: the filters will be displayed right below each column's header cell. - * - [[FILTER_POS_FOOTER]]: the filters will be displayed below each column's footer cell. - */ - public $filterPosition = self::FILTER_POS_BODY; - /** - * @var array the HTML attributes for the filter row element. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $filterRowOptions = ['class' => 'filters']; - - - /** - * Initializes the grid view. - * This method will initialize required property values and instantiate [[columns]] objects. - */ - public function init() - { - parent::init(); - if ($this->formatter == null) { - $this->formatter = Yii::$app->getFormatter(); - } elseif (is_array($this->formatter)) { - $this->formatter = Yii::createObject($this->formatter); - } - if (!$this->formatter instanceof Formatter) { - throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); - } - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - if (!isset($this->filterRowOptions['id'])) { - $this->filterRowOptions['id'] = $this->options['id'] . '-filters'; - } - - $this->initColumns(); - } - - /** - * Runs the widget. - */ - public function run() - { - $id = $this->options['id']; - $options = Json::encode($this->getClientOptions()); - $view = $this->getView(); - GridViewAsset::register($view); - $view->registerJs("jQuery('#$id').yiiGridView($options);"); - parent::run(); - } - - /** - * Returns the options for the grid view JS widget. - * @return array the options - */ - protected function getClientOptions() - { - $filterUrl = isset($this->filterUrl) ? $this->filterUrl : [Yii::$app->controller->action->id]; - $id = $this->filterRowOptions['id']; - $filterSelector = "#$id input, #$id select"; - if (isset($this->filterSelector)) { - $filterSelector .= ', ' . $this->filterSelector; - } - - return [ - 'filterUrl' => Url::to($filterUrl), - 'filterSelector' => $filterSelector, - ]; - } - - /** - * Renders the data models for the grid view. - */ - public function renderItems() - { - $content = array_filter([ - $this->renderCaption(), - $this->renderColumnGroup(), - $this->showHeader ? $this->renderTableHeader() : false, - $this->showFooter ? $this->renderTableFooter() : false, - $this->renderTableBody(), - ]); - return Html::tag('table', implode("\n", $content), $this->tableOptions); - } - - /** - * Renders the caption element. - * @return bool|string the rendered caption element or `false` if no caption element should be rendered. - */ - public function renderCaption() - { - if (!empty($this->caption)) { - return Html::tag('caption', $this->caption, $this->captionOptions); - } else { - return false; - } - } - - /** - * Renders the column group HTML. - * @return bool|string the column group HTML or `false` if no column group should be rendered. - */ - public function renderColumnGroup() - { - $requireColumnGroup = false; - foreach ($this->columns as $column) { - /** @var Column $column */ - if (!empty($column->options)) { - $requireColumnGroup = true; - break; - } - } - if ($requireColumnGroup) { - $cols = []; - foreach ($this->columns as $column) { - $cols[] = Html::tag('col', '', $column->options); - } - return Html::tag('colgroup', implode("\n", $cols)); - } else { - return false; - } - } - - /** - * Renders the table header. - * @return string the rendering result. - */ - public function renderTableHeader() - { - $cells = []; - foreach ($this->columns as $column) { - /** @var Column $column */ - $cells[] = $column->renderHeaderCell(); - } - $content = Html::tag('tr', implode('', $cells), $this->headerRowOptions); - if ($this->filterPosition == self::FILTER_POS_HEADER) { - $content = $this->renderFilters() . $content; - } elseif ($this->filterPosition == self::FILTER_POS_BODY) { - $content .= $this->renderFilters(); - } - - return "\n" . $content . "\n"; - } - - /** - * Renders the table footer. - * @return string the rendering result. - */ - public function renderTableFooter() - { - $cells = []; - foreach ($this->columns as $column) { - /** @var Column $column */ - $cells[] = $column->renderFooterCell(); - } - $content = Html::tag('tr', implode('', $cells), $this->footerRowOptions); - if ($this->filterPosition == self::FILTER_POS_FOOTER) { - $content .= $this->renderFilters(); - } - return "\n" . $content . "\n"; - } - - /** - * Renders the filter. - * @return string the rendering result. - */ - public function renderFilters() - { - if ($this->filterModel !== null) { - $cells = []; - foreach ($this->columns as $column) { - /** @var Column $column */ - $cells[] = $column->renderFilterCell(); - } - return Html::tag('tr', implode('', $cells), $this->filterRowOptions); - } else { - return ''; - } - } - - /** - * Renders the table body. - * @return string the rendering result. - */ - public function renderTableBody() - { - $models = array_values($this->dataProvider->getModels()); - $keys = $this->dataProvider->getKeys(); - $rows = []; - foreach ($models as $index => $model) { - $key = $keys[$index]; - if ($this->beforeRow !== null) { - $row = call_user_func($this->beforeRow, $model, $key, $index, $this); - if (!empty($row)) { - $rows[] = $row; - } - } - - $rows[] = $this->renderTableRow($model, $key, $index); - - if ($this->afterRow !== null) { - $row = call_user_func($this->afterRow, $model, $key, $index, $this); - if (!empty($row)) { - $rows[] = $row; - } - } - } - - if (empty($rows)) { - $colspan = count($this->columns); - return "\n" . $this->renderEmpty() . "\n"; - } else { - return "\n" . implode("\n", $rows) . "\n"; - } - } - - /** - * Renders a table row with the given data model and key. - * @param mixed $model the data model to be rendered - * @param mixed $key the key associated with the data model - * @param integer $index the zero-based index of the data model among the model array returned by [[dataProvider]]. - * @return string the rendering result - */ - public function renderTableRow($model, $key, $index) - { - $cells = []; - /** @var Column $column */ - foreach ($this->columns as $column) { - $cells[] = $column->renderDataCell($model, $key, $index); - } - if ($this->rowOptions instanceof Closure) { - $options = call_user_func($this->rowOptions, $model, $key, $index, $this); - } else { - $options = $this->rowOptions; - } - $options['data-key'] = is_array($key) ? json_encode($key) : (string)$key; - return Html::tag('tr', implode('', $cells), $options); - } - - /** - * Creates column objects and initializes them. - */ - protected function initColumns() - { - if (empty($this->columns)) { - $this->guessColumns(); - } - foreach ($this->columns as $i => $column) { - if (is_string($column)) { - $column = $this->createDataColumn($column); - } else { - $column = Yii::createObject(array_merge([ - 'class' => $this->dataColumnClass ?: DataColumn::className(), - 'grid' => $this, - ], $column)); - } - if (!$column->visible) { - unset($this->columns[$i]); - continue; - } - $this->columns[$i] = $column; - } - } - - /** - * Creates a [[DataColumn]] object based on a string in the format of "attribute:format:label". - * @param string $text the column specification string - * @return DataColumn the column instance - * @throws InvalidConfigException if the column specification is invalid - */ - protected function createDataColumn($text) - { - if (!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/', $text, $matches)) { - throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"'); - } - return Yii::createObject([ - 'class' => $this->dataColumnClass ?: DataColumn::className(), - 'grid' => $this, - 'attribute' => $matches[1], - 'format' => isset($matches[3]) ? $matches[3] : 'text', - 'label' => isset($matches[5]) ? $matches[5] : null, - ]); - } - - /** - * This function tries to guesses the columns to show from the given data - * if [[columns]] are not explicitly specified. - */ - protected function guessColumns() - { - $models = $this->dataProvider->getModels(); - $model = reset($models); - if (is_array($model) || is_object($model)) { - foreach ($model as $name => $value) { - $this->columns[] = $name; - } - } else { - throw new InvalidConfigException('Unable to generate columns from data.'); - } - } + const FILTER_POS_HEADER = 'header'; + const FILTER_POS_FOOTER = 'footer'; + const FILTER_POS_BODY = 'body'; + + /** + * @var string the default data column class if the class name is not explicitly specified when configuring a data column. + * Defaults to 'yii\grid\DataColumn'. + */ + public $dataColumnClass; + /** + * @var string the caption of the grid table + * @see captionOptions + */ + public $caption; + /** + * @var array the HTML attributes for the caption element. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + * @see caption + */ + public $captionOptions = []; + /** + * @var array the HTML attributes for the grid table element. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $tableOptions = ['class' => 'table table-striped table-bordered']; + /** + * @var array the HTML attributes for the container tag of the grid view. + * The "tag" element specifies the tag name of the container element and defaults to "div". + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'grid-view']; + /** + * @var array the HTML attributes for the table header row. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerRowOptions = []; + /** + * @var array the HTML attributes for the table footer row. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $footerRowOptions = []; + /** + * @var array|Closure the HTML attributes for the table body rows. This can be either an array + * specifying the common HTML attributes for all body rows, or an anonymous function that + * returns an array of the HTML attributes. The anonymous function will be called once for every + * data model returned by [[dataProvider]]. It should have the following signature: + * + * ```php + * function ($model, $key, $index, $grid) + * ``` + * + * - `$model`: the current data model being rendered + * - `$key`: the key value associated with the current data model + * - `$index`: the zero-based index of the data model in the model array returned by [[dataProvider]] + * - `$grid`: the GridView object + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $rowOptions = []; + /** + * @var Closure an anonymous function that is called once BEFORE rendering each data model. + * It should have the similar signature as [[rowOptions]]. The return result of the function + * will be rendered directly. + */ + public $beforeRow; + /** + * @var Closure an anonymous function that is called once AFTER rendering each data model. + * It should have the similar signature as [[rowOptions]]. The return result of the function + * will be rendered directly. + */ + public $afterRow; + /** + * @var boolean whether to show the header section of the grid table. + */ + public $showHeader = true; + /** + * @var boolean whether to show the footer section of the grid table. + */ + public $showFooter = false; + /** + * @var boolean whether to show the grid view if [[dataProvider]] returns no data. + */ + public $showOnEmpty = true; + /** + * @var array|Formatter the formatter used to format model attribute values into displayable texts. + * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] + * instance. If this property is not set, the "formatter" application component will be used. + */ + public $formatter; + /** + * @var array grid column configuration. Each array element represents the configuration + * for one particular grid column. For example, + * + * ```php + * [ + * ['class' => SerialColumn::className()], + * [ + * 'class' => DataColumn::className(), + * 'attribute' => 'name', + * 'format' => 'text', + * 'label' => 'Name', + * ], + * ['class' => CheckboxColumn::className()], + * ] + * ``` + * + * If a column is of class [[DataColumn]], the "class" element can be omitted. + * + * As a shortcut format, a string may be used to specify the configuration of a data column + * which only contains "attribute", "format", and/or "label" options: `"attribute:format:label"`. + * For example, the above "name" column can also be specified as: `"name:text:Name"`. + * Both "format" and "label" are optional. They will take default values if absent. + */ + public $columns = []; + public $emptyCell = ' '; + /** + * @var \yii\base\Model the model that keeps the user-entered filter data. When this property is set, + * the grid view will enable column-based filtering. Each data column by default will display a text field + * at the top that users can fill in to filter the data. + * + * Note that in order to show an input field for filtering, a column must have its [[DataColumn::attribute]] + * property set or have [[DataColumn::filter]] set as the HTML code for the input field. + * + * When this property is not set (null) the filtering feature is disabled. + */ + public $filterModel; + /** + * @var string|array the URL for returning the filtering result. [[Url::to()]] will be called to + * normalize the URL. If not set, the current controller action will be used. + * When the user makes change to any filter input, the current filtering inputs will be appended + * as GET parameters to this URL. + */ + public $filterUrl; + public $filterSelector; + /** + * @var string whether the filters should be displayed in the grid view. Valid values include: + * + * - [[FILTER_POS_HEADER]]: the filters will be displayed on top of each column's header cell. + * - [[FILTER_POS_BODY]]: the filters will be displayed right below each column's header cell. + * - [[FILTER_POS_FOOTER]]: the filters will be displayed below each column's footer cell. + */ + public $filterPosition = self::FILTER_POS_BODY; + /** + * @var array the HTML attributes for the filter row element. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $filterRowOptions = ['class' => 'filters']; + + + /** + * Initializes the grid view. + * This method will initialize required property values and instantiate [[columns]] objects. + */ + public function init() + { + parent::init(); + if ($this->formatter == null) { + $this->formatter = Yii::$app->getFormatter(); + } elseif (is_array($this->formatter)) { + $this->formatter = Yii::createObject($this->formatter); + } + if (!$this->formatter instanceof Formatter) { + throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); + } + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + if (!isset($this->filterRowOptions['id'])) { + $this->filterRowOptions['id'] = $this->options['id'] . '-filters'; + } + + $this->initColumns(); + } + + /** + * Runs the widget. + */ + public function run() + { + $id = $this->options['id']; + $options = Json::encode($this->getClientOptions()); + $view = $this->getView(); + GridViewAsset::register($view); + $view->registerJs("jQuery('#$id').yiiGridView($options);"); + parent::run(); + } + + /** + * Returns the options for the grid view JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $filterUrl = isset($this->filterUrl) ? $this->filterUrl : [Yii::$app->controller->action->id]; + $id = $this->filterRowOptions['id']; + $filterSelector = "#$id input, #$id select"; + if (isset($this->filterSelector)) { + $filterSelector .= ', ' . $this->filterSelector; + } + + return [ + 'filterUrl' => Url::to($filterUrl), + 'filterSelector' => $filterSelector, + ]; + } + + /** + * Renders the data models for the grid view. + */ + public function renderItems() + { + $content = array_filter([ + $this->renderCaption(), + $this->renderColumnGroup(), + $this->showHeader ? $this->renderTableHeader() : false, + $this->showFooter ? $this->renderTableFooter() : false, + $this->renderTableBody(), + ]); + + return Html::tag('table', implode("\n", $content), $this->tableOptions); + } + + /** + * Renders the caption element. + * @return bool|string the rendered caption element or `false` if no caption element should be rendered. + */ + public function renderCaption() + { + if (!empty($this->caption)) { + return Html::tag('caption', $this->caption, $this->captionOptions); + } else { + return false; + } + } + + /** + * Renders the column group HTML. + * @return bool|string the column group HTML or `false` if no column group should be rendered. + */ + public function renderColumnGroup() + { + $requireColumnGroup = false; + foreach ($this->columns as $column) { + /** @var Column $column */ + if (!empty($column->options)) { + $requireColumnGroup = true; + break; + } + } + if ($requireColumnGroup) { + $cols = []; + foreach ($this->columns as $column) { + $cols[] = Html::tag('col', '', $column->options); + } + + return Html::tag('colgroup', implode("\n", $cols)); + } else { + return false; + } + } + + /** + * Renders the table header. + * @return string the rendering result. + */ + public function renderTableHeader() + { + $cells = []; + foreach ($this->columns as $column) { + /** @var Column $column */ + $cells[] = $column->renderHeaderCell(); + } + $content = Html::tag('tr', implode('', $cells), $this->headerRowOptions); + if ($this->filterPosition == self::FILTER_POS_HEADER) { + $content = $this->renderFilters() . $content; + } elseif ($this->filterPosition == self::FILTER_POS_BODY) { + $content .= $this->renderFilters(); + } + + return "\n" . $content . "\n"; + } + + /** + * Renders the table footer. + * @return string the rendering result. + */ + public function renderTableFooter() + { + $cells = []; + foreach ($this->columns as $column) { + /** @var Column $column */ + $cells[] = $column->renderFooterCell(); + } + $content = Html::tag('tr', implode('', $cells), $this->footerRowOptions); + if ($this->filterPosition == self::FILTER_POS_FOOTER) { + $content .= $this->renderFilters(); + } + + return "\n" . $content . "\n"; + } + + /** + * Renders the filter. + * @return string the rendering result. + */ + public function renderFilters() + { + if ($this->filterModel !== null) { + $cells = []; + foreach ($this->columns as $column) { + /** @var Column $column */ + $cells[] = $column->renderFilterCell(); + } + + return Html::tag('tr', implode('', $cells), $this->filterRowOptions); + } else { + return ''; + } + } + + /** + * Renders the table body. + * @return string the rendering result. + */ + public function renderTableBody() + { + $models = array_values($this->dataProvider->getModels()); + $keys = $this->dataProvider->getKeys(); + $rows = []; + foreach ($models as $index => $model) { + $key = $keys[$index]; + if ($this->beforeRow !== null) { + $row = call_user_func($this->beforeRow, $model, $key, $index, $this); + if (!empty($row)) { + $rows[] = $row; + } + } + + $rows[] = $this->renderTableRow($model, $key, $index); + + if ($this->afterRow !== null) { + $row = call_user_func($this->afterRow, $model, $key, $index, $this); + if (!empty($row)) { + $rows[] = $row; + } + } + } + + if (empty($rows)) { + $colspan = count($this->columns); + + return "\n" . $this->renderEmpty() . "\n"; + } else { + return "\n" . implode("\n", $rows) . "\n"; + } + } + + /** + * Renders a table row with the given data model and key. + * @param mixed $model the data model to be rendered + * @param mixed $key the key associated with the data model + * @param integer $index the zero-based index of the data model among the model array returned by [[dataProvider]]. + * @return string the rendering result + */ + public function renderTableRow($model, $key, $index) + { + $cells = []; + /** @var Column $column */ + foreach ($this->columns as $column) { + $cells[] = $column->renderDataCell($model, $key, $index); + } + if ($this->rowOptions instanceof Closure) { + $options = call_user_func($this->rowOptions, $model, $key, $index, $this); + } else { + $options = $this->rowOptions; + } + $options['data-key'] = is_array($key) ? json_encode($key) : (string) $key; + + return Html::tag('tr', implode('', $cells), $options); + } + + /** + * Creates column objects and initializes them. + */ + protected function initColumns() + { + if (empty($this->columns)) { + $this->guessColumns(); + } + foreach ($this->columns as $i => $column) { + if (is_string($column)) { + $column = $this->createDataColumn($column); + } else { + $column = Yii::createObject(array_merge([ + 'class' => $this->dataColumnClass ?: DataColumn::className(), + 'grid' => $this, + ], $column)); + } + if (!$column->visible) { + unset($this->columns[$i]); + continue; + } + $this->columns[$i] = $column; + } + } + + /** + * Creates a [[DataColumn]] object based on a string in the format of "attribute:format:label". + * @param string $text the column specification string + * @return DataColumn the column instance + * @throws InvalidConfigException if the column specification is invalid + */ + protected function createDataColumn($text) + { + if (!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/', $text, $matches)) { + throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"'); + } + + return Yii::createObject([ + 'class' => $this->dataColumnClass ?: DataColumn::className(), + 'grid' => $this, + 'attribute' => $matches[1], + 'format' => isset($matches[3]) ? $matches[3] : 'text', + 'label' => isset($matches[5]) ? $matches[5] : null, + ]); + } + + /** + * This function tries to guesses the columns to show from the given data + * if [[columns]] are not explicitly specified. + */ + protected function guessColumns() + { + $models = $this->dataProvider->getModels(); + $model = reset($models); + if (is_array($model) || is_object($model)) { + foreach ($model as $name => $value) { + $this->columns[] = $name; + } + } else { + throw new InvalidConfigException('Unable to generate columns from data.'); + } + } } diff --git a/framework/grid/GridViewAsset.php b/framework/grid/GridViewAsset.php index a67999d3104..0403e9f5b76 100644 --- a/framework/grid/GridViewAsset.php +++ b/framework/grid/GridViewAsset.php @@ -17,11 +17,11 @@ */ class GridViewAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'yii.gridView.js', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'yii.gridView.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/framework/grid/SerialColumn.php b/framework/grid/SerialColumn.php index e5dc74cbc92..50a3f6bba9b 100644 --- a/framework/grid/SerialColumn.php +++ b/framework/grid/SerialColumn.php @@ -27,18 +27,18 @@ */ class SerialColumn extends Column { - public $header = '#'; + public $header = '#'; - /** - * @inheritdoc - */ - protected function renderDataCellContent($model, $key, $index) - { - $pagination = $this->grid->dataProvider->getPagination(); - if ($pagination !== false) { - return $pagination->getOffset() + $index + 1; - } else { - return $index + 1; - } - } + /** + * @inheritdoc + */ + protected function renderDataCellContent($model, $key, $index) + { + $pagination = $this->grid->dataProvider->getPagination(); + if ($pagination !== false) { + return $pagination->getOffset() + $index + 1; + } else { + return $index + 1; + } + } } diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index 278eaf096c6..d0b1a8f5cab 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -21,462 +21,473 @@ */ class BaseArrayHelper { - /** - * Converts an object or an array of objects into an array. - * @param object|array $object the object to be converted into an array - * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays. - * The properties specified for each class is an array of the following format: - * - * ~~~ - * [ - * 'app\models\Post' => [ - * 'id', - * 'title', - * // the key name in array result => property name - * 'createTime' => 'created_at', - * // the key name in array result => anonymous function - * 'length' => function ($post) { - * return strlen($post->content); - * }, - * ], - * ] - * ~~~ - * - * The result of `ArrayHelper::toArray($post, $properties)` could be like the following: - * - * ~~~ - * [ - * 'id' => 123, - * 'title' => 'test', - * 'createTime' => '2013-01-01 12:00AM', - * 'length' => 301, - * ] - * ~~~ - * - * @param boolean $recursive whether to recursively converts properties which are objects into arrays. - * @return array the array representation of the object - */ - public static function toArray($object, $properties = [], $recursive = true) - { - if (is_array($object)) { - if ($recursive) { - foreach ($object as $key => $value) { - if (is_array($value) || is_object($value)) { - $object[$key] = static::toArray($value, true); - } - } - } - return $object; - } elseif (is_object($object)) { - if (!empty($properties)) { - $className = get_class($object); - if (!empty($properties[$className])) { - $result = []; - foreach ($properties[$className] as $key => $name) { - if (is_int($key)) { - $result[$name] = $object->$name; - } else { - $result[$key] = static::getValue($object, $name); - } - } - return $recursive ? static::toArray($result) : $result; - } - } - if ($object instanceof Arrayable) { - $result = $object->toArray(); - } else { - $result = []; - foreach ($object as $key => $value) { - $result[$key] = $value; - } - } - return $recursive ? static::toArray($result) : $result; - } else { - return [$object]; - } - } + /** + * Converts an object or an array of objects into an array. + * @param object|array $object the object to be converted into an array + * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays. + * The properties specified for each class is an array of the following format: + * + * ~~~ + * [ + * 'app\models\Post' => [ + * 'id', + * 'title', + * // the key name in array result => property name + * 'createTime' => 'created_at', + * // the key name in array result => anonymous function + * 'length' => function ($post) { + * return strlen($post->content); + * }, + * ], + * ] + * ~~~ + * + * The result of `ArrayHelper::toArray($post, $properties)` could be like the following: + * + * ~~~ + * [ + * 'id' => 123, + * 'title' => 'test', + * 'createTime' => '2013-01-01 12:00AM', + * 'length' => 301, + * ] + * ~~~ + * + * @param boolean $recursive whether to recursively converts properties which are objects into arrays. + * @return array the array representation of the object + */ + public static function toArray($object, $properties = [], $recursive = true) + { + if (is_array($object)) { + if ($recursive) { + foreach ($object as $key => $value) { + if (is_array($value) || is_object($value)) { + $object[$key] = static::toArray($value, true); + } + } + } - /** - * Merges two or more arrays into one recursively. - * If each array has an element with the same string key value, the latter - * will overwrite the former (different from array_merge_recursive). - * Recursive merging will be conducted if both arrays have an element of array - * type and are having the same key. - * For integer-keyed elements, the elements from the latter array will - * be appended to the former array. - * @param array $a array to be merged to - * @param array $b array to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array the merged array (the original arrays are not changed.) - */ - public static function merge($a, $b) - { - $args = func_get_args(); - $res = array_shift($args); - while (!empty($args)) { - $next = array_shift($args); - foreach ($next as $k => $v) { - if (is_integer($k)) { - isset($res[$k]) ? $res[] = $v : $res[$k] = $v; - } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { - $res[$k] = self::merge($res[$k], $v); - } else { - $res[$k] = $v; - } - } - } - return $res; - } + return $object; + } elseif (is_object($object)) { + if (!empty($properties)) { + $className = get_class($object); + if (!empty($properties[$className])) { + $result = []; + foreach ($properties[$className] as $key => $name) { + if (is_int($key)) { + $result[$name] = $object->$name; + } else { + $result[$key] = static::getValue($object, $name); + } + } - /** - * Retrieves the value of an array element or object property with the given key or property name. - * If the key does not exist in the array or object, the default value will be returned instead. - * - * The key may be specified in a dot format to retrieve the value of a sub-array or the property - * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would - * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']` - * or `$array->x` is neither an array nor an object, the default value will be returned. - * Note that if the array already has an element `x.y.z`, then its value will be returned - * instead of going through the sub-arrays. - * - * Below are some usage examples, - * - * ~~~ - * // working with array - * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); - * // working with object - * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); - * // working with anonymous function - * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { - * return $user->firstName . ' ' . $user->lastName; - * }); - * // using dot format to retrieve the property of embedded object - * $street = \yii\helpers\ArrayHelper::getValue($users, 'address.street'); - * ~~~ - * - * @param array|object $array array or object to extract value from - * @param string|\Closure $key key name of the array element, or property name of the object, - * or an anonymous function returning the value. The anonymous function signature should be: - * `function($array, $defaultValue)`. - * @param mixed $default the default value to be returned if the specified key does not exist - * @return mixed the value of the element if found, default value otherwise - * @throws InvalidParamException if $array is neither an array nor an object. - */ - public static function getValue($array, $key, $default = null) - { - if ($key instanceof \Closure) { - return $key($array, $default); - } + return $recursive ? static::toArray($result) : $result; + } + } + if ($object instanceof Arrayable) { + $result = $object->toArray(); + } else { + $result = []; + foreach ($object as $key => $value) { + $result[$key] = $value; + } + } - if (is_array($array) && array_key_exists($key, $array)) { - return $array[$key]; - } + return $recursive ? static::toArray($result) : $result; + } else { + return [$object]; + } + } - if (($pos = strrpos($key, '.')) !== false) { - $array = static::getValue($array, substr($key, 0, $pos), $default); - $key = substr($key, $pos + 1); - } + /** + * Merges two or more arrays into one recursively. + * If each array has an element with the same string key value, the latter + * will overwrite the former (different from array_merge_recursive). + * Recursive merging will be conducted if both arrays have an element of array + * type and are having the same key. + * For integer-keyed elements, the elements from the latter array will + * be appended to the former array. + * @param array $a array to be merged to + * @param array $b array to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array the merged array (the original arrays are not changed.) + */ + public static function merge($a, $b) + { + $args = func_get_args(); + $res = array_shift($args); + while (!empty($args)) { + $next = array_shift($args); + foreach ($next as $k => $v) { + if (is_integer($k)) { + isset($res[$k]) ? $res[] = $v : $res[$k] = $v; + } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { + $res[$k] = self::merge($res[$k], $v); + } else { + $res[$k] = $v; + } + } + } - if (is_object($array)) { - return $array->$key; - } elseif (is_array($array)) { - return array_key_exists($key, $array) ? $array[$key] : $default; - } else { - return $default; - } - } + return $res; + } - /** - * Removes an item from an array and returns the value. If the key does not exist in the array, the default value - * will be returned instead. - * - * Usage examples, - * - * ~~~ - * // $array = ['type' => 'A', 'options' => [1, 2]]; - * // working with array - * $type = \yii\helpers\ArrayHelper::remove($array, 'type'); - * // $array content - * // $array = ['options' => [1, 2]]; - * ~~~ - * - * @param array $array the array to extract value from - * @param string $key key name of the array element - * @param mixed $default the default value to be returned if the specified key does not exist - * @return mixed|null the value of the element if found, default value otherwise - */ - public static function remove(&$array, $key, $default = null) - { - if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { - $value = $array[$key]; - unset($array[$key]); - return $value; - } - return $default; - } + /** + * Retrieves the value of an array element or object property with the given key or property name. + * If the key does not exist in the array or object, the default value will be returned instead. + * + * The key may be specified in a dot format to retrieve the value of a sub-array or the property + * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would + * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']` + * or `$array->x` is neither an array nor an object, the default value will be returned. + * Note that if the array already has an element `x.y.z`, then its value will be returned + * instead of going through the sub-arrays. + * + * Below are some usage examples, + * + * ~~~ + * // working with array + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); + * // working with object + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); + * // working with anonymous function + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function ($user, $defaultValue) { + * return $user->firstName . ' ' . $user->lastName; + * }); + * // using dot format to retrieve the property of embedded object + * $street = \yii\helpers\ArrayHelper::getValue($users, 'address.street'); + * ~~~ + * + * @param array|object $array array or object to extract value from + * @param string|\Closure $key key name of the array element, or property name of the object, + * or an anonymous function returning the value. The anonymous function signature should be: + * `function($array, $defaultValue)`. + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed the value of the element if found, default value otherwise + * @throws InvalidParamException if $array is neither an array nor an object. + */ + public static function getValue($array, $key, $default = null) + { + if ($key instanceof \Closure) { + return $key($array, $default); + } - /** - * Indexes an array according to a specified key. - * The input array should be multidimensional or an array of objects. - * - * The key can be a key name of the sub-array, a property name of object, or an anonymous - * function which returns the key value given an array element. - * - * If a key value is null, the corresponding array element will be discarded and not put in the result. - * - * For example, - * - * ~~~ - * $array = [ - * ['id' => '123', 'data' => 'abc'], - * ['id' => '345', 'data' => 'def'], - * ]; - * $result = ArrayHelper::index($array, 'id'); - * // the result is: - * // [ - * // '123' => ['id' => '123', 'data' => 'abc'], - * // '345' => ['id' => '345', 'data' => 'def'], - * // ] - * - * // using anonymous function - * $result = ArrayHelper::index($array, function ($element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array the array that needs to be indexed - * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array - * @return array the indexed array - */ - public static function index($array, $key) - { - $result = []; - foreach ($array as $element) { - $value = static::getValue($element, $key); - $result[$value] = $element; - } - return $result; - } + if (is_array($array) && array_key_exists($key, $array)) { + return $array[$key]; + } - /** - * Returns the values of a specified column in an array. - * The input array should be multidimensional or an array of objects. - * - * For example, - * - * ~~~ - * $array = [ - * ['id' => '123', 'data' => 'abc'], - * ['id' => '345', 'data' => 'def'], - * ]; - * $result = ArrayHelper::getColumn($array, 'id'); - * // the result is: ['123', '345'] - * - * // using anonymous function - * $result = ArrayHelper::getColumn($array, function ($element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array - * @param string|\Closure $name - * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array - * will be re-indexed with integers. - * @return array the list of column values - */ - public static function getColumn($array, $name, $keepKeys = true) - { - $result = []; - if ($keepKeys) { - foreach ($array as $k => $element) { - $result[$k] = static::getValue($element, $name); - } - } else { - foreach ($array as $element) { - $result[] = static::getValue($element, $name); - } - } + if (($pos = strrpos($key, '.')) !== false) { + $array = static::getValue($array, substr($key, 0, $pos), $default); + $key = substr($key, $pos + 1); + } - return $result; - } + if (is_object($array)) { + return $array->$key; + } elseif (is_array($array)) { + return array_key_exists($key, $array) ? $array[$key] : $default; + } else { + return $default; + } + } - /** - * Builds a map (key-value pairs) from a multidimensional array or an array of objects. - * The `$from` and `$to` parameters specify the key names or property names to set up the map. - * Optionally, one can further group the map according to a grouping field `$group`. - * - * For example, - * - * ~~~ - * $array = [ - * ['id' => '123', 'name' => 'aaa', 'class' => 'x'], - * ['id' => '124', 'name' => 'bbb', 'class' => 'x'], - * ['id' => '345', 'name' => 'ccc', 'class' => 'y'], - * ); - * - * $result = ArrayHelper::map($array, 'id', 'name'); - * // the result is: - * // [ - * // '123' => 'aaa', - * // '124' => 'bbb', - * // '345' => 'ccc', - * // ] - * - * $result = ArrayHelper::map($array, 'id', 'name', 'class'); - * // the result is: - * // [ - * // 'x' => [ - * // '123' => 'aaa', - * // '124' => 'bbb', - * // ], - * // 'y' => [ - * // '345' => 'ccc', - * // ], - * // ] - * ~~~ - * - * @param array $array - * @param string|\Closure $from - * @param string|\Closure $to - * @param string|\Closure $group - * @return array - */ - public static function map($array, $from, $to, $group = null) - { - $result = []; - foreach ($array as $element) { - $key = static::getValue($element, $from); - $value = static::getValue($element, $to); - if ($group !== null) { - $result[static::getValue($element, $group)][$key] = $value; - } else { - $result[$key] = $value; - } - } - return $result; - } + /** + * Removes an item from an array and returns the value. If the key does not exist in the array, the default value + * will be returned instead. + * + * Usage examples, + * + * ~~~ + * // $array = ['type' => 'A', 'options' => [1, 2]]; + * // working with array + * $type = \yii\helpers\ArrayHelper::remove($array, 'type'); + * // $array content + * // $array = ['options' => [1, 2]]; + * ~~~ + * + * @param array $array the array to extract value from + * @param string $key key name of the array element + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed|null the value of the element if found, default value otherwise + */ + public static function remove(&$array, $key, $default = null) + { + if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { + $value = $array[$key]; + unset($array[$key]); - /** - * Checks if the given array contains the specified key. - * This method enhances the `array_key_exists()` function by supporting case-insensitive - * key comparison. - * @param string $key the key to check - * @param array $array the array with keys to check - * @param boolean $caseSensitive whether the key comparison should be case-sensitive - * @return boolean whether the array contains the specified key - */ - public static function keyExists($key, $array, $caseSensitive = true) - { - if ($caseSensitive) { - return array_key_exists($key, $array); - } else { - foreach (array_keys($array) as $k) { - if (strcasecmp($key, $k) === 0) { - return true; - } - } - return false; - } - } + return $value; + } - /** - * Sorts an array of objects or arrays (with the same structure) by one or several keys. - * @param array $array the array to be sorted. The array will be modified after calling this method. - * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array - * elements, a property name of the objects, or an anonymous function returning the values for comparison - * purpose. The anonymous function signature should be: `function($item)`. - * To sort by multiple keys, provide an array of keys here. - * @param integer|array $direction the sorting direction. It can be either `SORT_ASC` or `SORT_DESC`. - * When sorting by multiple keys with different sorting directions, use an array of sorting directions. - * @param integer|array $sortFlag the PHP sort flag. Valid values include - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, `SORT_LOCALE_STRING`, `SORT_NATURAL` and `SORT_FLAG_CASE`. - * Please refer to [PHP manual](http://php.net/manual/en/function.sort.php) - * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags. - * @throws InvalidParamException if the $descending or $sortFlag parameters do not have - * correct number of elements as that of $key. - */ - public static function multisort(&$array, $key, $direction = SORT_ASC, $sortFlag = SORT_REGULAR) - { - $keys = is_array($key) ? $key : [$key]; - if (empty($keys) || empty($array)) { - return; - } - $n = count($keys); - if (is_scalar($direction)) { - $direction = array_fill(0, $n, $direction); - } elseif (count($direction) !== $n) { - throw new InvalidParamException('The length of $descending parameter must be the same as that of $keys.'); - } - if (is_scalar($sortFlag)) { - $sortFlag = array_fill(0, $n, $sortFlag); - } elseif (count($sortFlag) !== $n) { - throw new InvalidParamException('The length of $sortFlag parameter must be the same as that of $keys.'); - } - $args = []; - foreach ($keys as $i => $key) { - $flag = $sortFlag[$i]; - $args[] = static::getColumn($array, $key); - $args[] = $direction[$i]; - $args[] = $flag; - } - $args[] = &$array; - call_user_func_array('array_multisort', $args); - } + return $default; + } - /** - * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded. - * If a value is an array, this method will also encode it recursively. - * @param array $data data to be encoded - * @param boolean $valuesOnly whether to encode array values only. If false, - * both the array keys and array values will be encoded. - * @param string $charset the charset that the data is using. If not set, - * [[\yii\base\Application::charset]] will be used. - * @return array the encoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function htmlEncode($data, $valuesOnly = true, $charset = null) - { - if ($charset === null) { - $charset = Yii::$app->charset; - } - $d = []; - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars($key, ENT_QUOTES, $charset); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); - } elseif (is_array($value)) { - $d[$key] = static::htmlEncode($value, $charset); - } - } - return $d; - } + /** + * Indexes an array according to a specified key. + * The input array should be multidimensional or an array of objects. + * + * The key can be a key name of the sub-array, a property name of object, or an anonymous + * function which returns the key value given an array element. + * + * If a key value is null, the corresponding array element will be discarded and not put in the result. + * + * For example, + * + * ~~~ + * $array = [ + * ['id' => '123', 'data' => 'abc'], + * ['id' => '345', 'data' => 'def'], + * ]; + * $result = ArrayHelper::index($array, 'id'); + * // the result is: + * // [ + * // '123' => ['id' => '123', 'data' => 'abc'], + * // '345' => ['id' => '345', 'data' => 'def'], + * // ] + * + * // using anonymous function + * $result = ArrayHelper::index($array, function ($element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array the array that needs to be indexed + * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array + * @return array the indexed array + */ + public static function index($array, $key) + { + $result = []; + foreach ($array as $element) { + $value = static::getValue($element, $key); + $result[$value] = $element; + } - /** - * Decodes HTML entities into the corresponding characters in an array of strings. - * Both the array keys and values will be decoded. - * If a value is an array, this method will also decode it recursively. - * @param array $data data to be decoded - * @param boolean $valuesOnly whether to decode array values only. If false, - * both the array keys and array values will be decoded. - * @return array the decoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function htmlDecode($data, $valuesOnly = true) - { - $d = []; - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars_decode($key, ENT_QUOTES); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); - } elseif (is_array($value)) { - $d[$key] = static::htmlDecode($value); - } - } - return $d; - } + return $result; + } + + /** + * Returns the values of a specified column in an array. + * The input array should be multidimensional or an array of objects. + * + * For example, + * + * ~~~ + * $array = [ + * ['id' => '123', 'data' => 'abc'], + * ['id' => '345', 'data' => 'def'], + * ]; + * $result = ArrayHelper::getColumn($array, 'id'); + * // the result is: ['123', '345'] + * + * // using anonymous function + * $result = ArrayHelper::getColumn($array, function ($element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array + * @param string|\Closure $name + * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array + * will be re-indexed with integers. + * @return array the list of column values + */ + public static function getColumn($array, $name, $keepKeys = true) + { + $result = []; + if ($keepKeys) { + foreach ($array as $k => $element) { + $result[$k] = static::getValue($element, $name); + } + } else { + foreach ($array as $element) { + $result[] = static::getValue($element, $name); + } + } + + return $result; + } + + /** + * Builds a map (key-value pairs) from a multidimensional array or an array of objects. + * The `$from` and `$to` parameters specify the key names or property names to set up the map. + * Optionally, one can further group the map according to a grouping field `$group`. + * + * For example, + * + * ~~~ + * $array = [ + * ['id' => '123', 'name' => 'aaa', 'class' => 'x'], + * ['id' => '124', 'name' => 'bbb', 'class' => 'x'], + * ['id' => '345', 'name' => 'ccc', 'class' => 'y'], + * ); + * + * $result = ArrayHelper::map($array, 'id', 'name'); + * // the result is: + * // [ + * // '123' => 'aaa', + * // '124' => 'bbb', + * // '345' => 'ccc', + * // ] + * + * $result = ArrayHelper::map($array, 'id', 'name', 'class'); + * // the result is: + * // [ + * // 'x' => [ + * // '123' => 'aaa', + * // '124' => 'bbb', + * // ], + * // 'y' => [ + * // '345' => 'ccc', + * // ], + * // ] + * ~~~ + * + * @param array $array + * @param string|\Closure $from + * @param string|\Closure $to + * @param string|\Closure $group + * @return array + */ + public static function map($array, $from, $to, $group = null) + { + $result = []; + foreach ($array as $element) { + $key = static::getValue($element, $from); + $value = static::getValue($element, $to); + if ($group !== null) { + $result[static::getValue($element, $group)][$key] = $value; + } else { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Checks if the given array contains the specified key. + * This method enhances the `array_key_exists()` function by supporting case-insensitive + * key comparison. + * @param string $key the key to check + * @param array $array the array with keys to check + * @param boolean $caseSensitive whether the key comparison should be case-sensitive + * @return boolean whether the array contains the specified key + */ + public static function keyExists($key, $array, $caseSensitive = true) + { + if ($caseSensitive) { + return array_key_exists($key, $array); + } else { + foreach (array_keys($array) as $k) { + if (strcasecmp($key, $k) === 0) { + return true; + } + } + + return false; + } + } + + /** + * Sorts an array of objects or arrays (with the same structure) by one or several keys. + * @param array $array the array to be sorted. The array will be modified after calling this method. + * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array + * elements, a property name of the objects, or an anonymous function returning the values for comparison + * purpose. The anonymous function signature should be: `function($item)`. + * To sort by multiple keys, provide an array of keys here. + * @param integer|array $direction the sorting direction. It can be either `SORT_ASC` or `SORT_DESC`. + * When sorting by multiple keys with different sorting directions, use an array of sorting directions. + * @param integer|array $sortFlag the PHP sort flag. Valid values include + * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, `SORT_LOCALE_STRING`, `SORT_NATURAL` and `SORT_FLAG_CASE`. + * Please refer to [PHP manual](http://php.net/manual/en/function.sort.php) + * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags. + * @throws InvalidParamException if the $descending or $sortFlag parameters do not have + * correct number of elements as that of $key. + */ + public static function multisort(&$array, $key, $direction = SORT_ASC, $sortFlag = SORT_REGULAR) + { + $keys = is_array($key) ? $key : [$key]; + if (empty($keys) || empty($array)) { + return; + } + $n = count($keys); + if (is_scalar($direction)) { + $direction = array_fill(0, $n, $direction); + } elseif (count($direction) !== $n) { + throw new InvalidParamException('The length of $descending parameter must be the same as that of $keys.'); + } + if (is_scalar($sortFlag)) { + $sortFlag = array_fill(0, $n, $sortFlag); + } elseif (count($sortFlag) !== $n) { + throw new InvalidParamException('The length of $sortFlag parameter must be the same as that of $keys.'); + } + $args = []; + foreach ($keys as $i => $key) { + $flag = $sortFlag[$i]; + $args[] = static::getColumn($array, $key); + $args[] = $direction[$i]; + $args[] = $flag; + } + $args[] = &$array; + call_user_func_array('array_multisort', $args); + } + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $valuesOnly = true, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = []; + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = true) + { + $d = []; + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); + } elseif (is_array($value)) { + $d[$key] = static::htmlDecode($value); + } + } + + return $d; + } } diff --git a/framework/helpers/BaseConsole.php b/framework/helpers/BaseConsole.php index 458f212958b..ad2b7a0f17c 100644 --- a/framework/helpers/BaseConsole.php +++ b/framework/helpers/BaseConsole.php @@ -17,906 +17,914 @@ */ class BaseConsole { - const FG_BLACK = 30; - const FG_RED = 31; - const FG_GREEN = 32; - const FG_YELLOW = 33; - const FG_BLUE = 34; - const FG_PURPLE = 35; - const FG_CYAN = 36; - const FG_GREY = 37; - - const BG_BLACK = 40; - const BG_RED = 41; - const BG_GREEN = 42; - const BG_YELLOW = 43; - const BG_BLUE = 44; - const BG_PURPLE = 45; - const BG_CYAN = 46; - const BG_GREY = 47; - - const RESET = 0; - const NORMAL = 0; - const BOLD = 1; - const ITALIC = 3; - const UNDERLINE = 4; - const BLINK = 5; - const NEGATIVE = 7; - const CONCEALED = 8; - const CROSSED_OUT = 9; - const FRAMED = 51; - const ENCIRCLED = 52; - const OVERLINED = 53; - - /** - * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved up - */ - public static function moveCursorUp($rows = 1) - { - echo "\033[" . (int)$rows . 'A'; - } - - /** - * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved down - */ - public static function moveCursorDown($rows = 1) - { - echo "\033[" . (int)$rows . 'B'; - } - - /** - * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved forward - */ - public static function moveCursorForward($steps = 1) - { - echo "\033[" . (int)$steps . 'C'; - } - - /** - * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved backward - */ - public static function moveCursorBackward($steps = 1) - { - echo "\033[" . (int)$steps . 'D'; - } - - /** - * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. - * @param integer $lines number of lines the cursor should be moved down - */ - public static function moveCursorNextLine($lines = 1) - { - echo "\033[" . (int)$lines . 'E'; - } - - /** - * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. - * @param integer $lines number of lines the cursor should be moved up - */ - public static function moveCursorPrevLine($lines = 1) - { - echo "\033[" . (int)$lines . 'F'; - } - - /** - * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. - * @param integer $column 1-based column number, 1 is the left edge of the screen. - * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - */ - public static function moveCursorTo($column, $row = null) - { - if ($row === null) { - echo "\033[" . (int)$column . 'G'; - } else { - echo "\033[" . (int)$row . ';' . (int)$column . 'H'; - } - } - - /** - * Scrolls whole page up by sending ANSI control code SU to the terminal. - * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. - * @param integer $lines number of lines to scroll up - */ - public static function scrollUp($lines = 1) - { - echo "\033[" . (int)$lines . "S"; - } - - /** - * Scrolls whole page down by sending ANSI control code SD to the terminal. - * New lines are added at the top. This is not supported by ANSI.SYS used in windows. - * @param integer $lines number of lines to scroll down - */ - public static function scrollDown($lines = 1) - { - echo "\033[" . (int)$lines . "T"; - } - - /** - * Saves the current cursor position by sending ANSI control code SCP to the terminal. - * Position can then be restored with [[restoreCursorPosition()]]. - */ - public static function saveCursorPosition() - { - echo "\033[s"; - } - - /** - * Restores the cursor position saved with [[saveCursorPosition()]] by sending ANSI control code RCP to the terminal. - */ - public static function restoreCursorPosition() - { - echo "\033[u"; - } - - /** - * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. - * Use [[showCursor()]] to bring it back. - * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. - */ - public static function hideCursor() - { - echo "\033[?25l"; - } - - /** - * Will show a cursor again when it has been hidden by [[hideCursor()]] by sending ANSI DECTCEM code ?25h to the terminal. - */ - public static function showCursor() - { - echo "\033[?25h"; - } - - /** - * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. - * Cursor position will not be changed. - * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. - */ - public static function clearScreen() - { - echo "\033[2J"; - } - - /** - * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenBeforeCursor() - { - echo "\033[1J"; - } - - /** - * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenAfterCursor() - { - echo "\033[0J"; - } - - /** - * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLine() - { - echo "\033[2K"; - } - - /** - * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineBeforeCursor() - { - echo "\033[1K"; - } - - /** - * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineAfterCursor() - { - echo "\033[0K"; - } - - /** - * Returns the ANSI format code. - * - * @param array $format An array containing formatting values. - * You can pass any of the FG_*, BG_* and TEXT_* constants - * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. - * @return string The ANSI format code according to the given formatting constants. - */ - public static function ansiFormatCode($format) - { - return "\033[" . implode(';', $format) . 'm'; - } - - /** - * Echoes an ANSI format code that affects the formatting of any text that is printed afterwards. - * - * @param array $format An array containing formatting values. - * You can pass any of the FG_*, BG_* and TEXT_* constants - * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. - * @see ansiFormatCode() - * @see endAnsiFormat() - */ - public static function beginAnsiFormat($format) - { - echo "\033[" . implode(';', $format) . 'm'; - } - - /** - * Resets any ANSI format set by previous method [[beginAnsiFormat()]] - * Any output after this will have default text format. - * This is equal to calling - * - * ```php - * echo Console::ansiFormatCode([Console::RESET]) - * ``` - */ - public static function endAnsiFormat() - { - echo "\033[0m"; - } - - /** - * Will return a string formatted with the given ANSI style - * - * @param string $string the string to be formatted - * @param array $format An array containing formatting values. - * You can pass any of the FG_*, BG_* and TEXT_* constants - * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. - * @return string - */ - public static function ansiFormat($string, $format = []) - { - $code = implode(';', $format); - return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string . "\033[0m"; - } - - /** - * Returns the ansi format code for xterm foreground color. - * You can pass the return value of this to one of the formatting methods: - * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]] - * - * @param integer $colorCode xterm color code - * @return string - * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - */ - public static function xtermFgColor($colorCode) - { - return '38;5;' . $colorCode; - } - - /** - * Returns the ansi format code for xterm background color. - * You can pass the return value of this to one of the formatting methods: - * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]] - * - * @param integer $colorCode xterm color code - * @return string - * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - */ - public static function xtermBgColor($colorCode) - { - return '48;5;' . $colorCode; - } - - /** - * Strips ANSI control codes from a string - * - * @param string $string String to strip - * @return string - */ - public static function stripAnsiFormat($string) - { - return preg_replace('/\033\[[\d;?]*\w/', '', $string); - } - - /** - * Converts an ANSI formatted string to HTML - * @param $string - * @return mixed - */ - // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746 - public static function ansiToHtml($string) - { - $tags = 0; - return preg_replace_callback( - '/\033\[[\d;]+m/', - function ($ansi) use (&$tags) { - $styleA = []; - foreach (explode(';', $ansi) as $controlCode) { - switch ($controlCode) { - case self::FG_BLACK: - $style = ['color' => '#000000']; - break; - case self::FG_BLUE: - $style = ['color' => '#000078']; - break; - case self::FG_CYAN: - $style = ['color' => '#007878']; - break; - case self::FG_GREEN: - $style = ['color' => '#007800']; - break; - case self::FG_GREY: - $style = ['color' => '#787878']; - break; - case self::FG_PURPLE: - $style = ['color' => '#780078']; - break; - case self::FG_RED: - $style = ['color' => '#780000']; - break; - case self::FG_YELLOW: - $style = ['color' => '#787800']; - break; - case self::BG_BLACK: - $style = ['background-color' => '#000000']; - break; - case self::BG_BLUE: - $style = ['background-color' => '#000078']; - break; - case self::BG_CYAN: - $style = ['background-color' => '#007878']; - break; - case self::BG_GREEN: - $style = ['background-color' => '#007800']; - break; - case self::BG_GREY: - $style = ['background-color' => '#787878']; - break; - case self::BG_PURPLE: - $style = ['background-color' => '#780078']; - break; - case self::BG_RED: - $style = ['background-color' => '#780000']; - break; - case self::BG_YELLOW: - $style = ['background-color' => '#787800']; - break; - case self::BOLD: - $style = ['font-weight' => 'bold']; - break; - case self::ITALIC: - $style = ['font-style' => 'italic']; - break; - case self::UNDERLINE: - $style = ['text-decoration' => ['underline']]; - break; - case self::OVERLINED: - $style = ['text-decoration' => ['overline']]; - break; - case self::CROSSED_OUT: - $style = ['text-decoration' => ['line-through']]; - break; - case self::BLINK: - $style = ['text-decoration' => ['blink']]; - break; - case self::NEGATIVE: // ??? - case self::CONCEALED: - case self::ENCIRCLED: - case self::FRAMED: - // TODO allow resetting codes - break; - case 0: // ansi reset - $return = ''; - for (; $tags > 0; $tags--) { - $return .= ''; - } - return $return; - } - - $styleA = ArrayHelper::merge($styleA, $style); - } - $styleString = []; - foreach ($styleA as $name => $content) { - if ($name === 'text-decoration') { - $content = implode(' ', $content); - } - $styleString[] = $name . ':' . $content; - } - $tags++; - return ' $value) { - static::output(" $key - $value"); - } - static::output(" ? - Show help"); - goto top; - } elseif (!in_array($input, array_keys($options))) { - goto top; - } - return $input; - } - - private static $_progressStart; - private static $_progressWidth; - private static $_progressPrefix; - - /** - * Starts display of a progress bar on screen. - * - * This bar will be updated by [[updateProgress()]] and my be ended by [[endProgress()]]. - * - * The following example shows a simple usage of a progress bar: - * - * ```php - * Console::startProgress(0, 1000); - * for ($n = 1; $n <= 1000; $n++) { - * usleep(1000); - * Console::updateProgress($n, 1000); - * } - * Console::endProgress(); - * ``` - * - * Git clone like progress (showing only status information): - * ```php - * Console::startProgress(0, 1000, 'Counting objects: ', false); - * for ($n = 1; $n <= 1000; $n++) { - * usleep(1000); - * Console::updateProgress($n, 1000); - * } - * Console::endProgress("done." . PHP_EOL); - * ``` - * - * @param integer $done the number of items that are completed. - * @param integer $total the total value of items that are to be done. - * @param string $prefix an optional string to display before the progress bar. - * Default to empty string which results in no prefix to be displayed. - * @param integer|boolean $width optional width of the progressbar. This can be an integer representing - * the number of characters to display for the progress bar or a float between 0 and 1 representing the - * percentage of screen with the progress bar may take. It can also be set to false to disable the - * bar and only show progress information like percent, number of items and ETA. - * If not set, the bar will be as wide as the screen. Screen size will be detected using [[getScreenSize()]]. - * @see startProgress - * @see updateProgress - * @see endProgress - */ - public static function startProgress($done, $total, $prefix = '', $width = null) - { - self::$_progressStart = time(); - self::$_progressWidth = $width; - self::$_progressPrefix = $prefix; - - static::updateProgress($done, $total); - } - - /** - * Updates a progress bar that has been started by [[startProgress()]]. - * - * @param integer $done the number of items that are completed. - * @param integer $total the total value of items that are to be done. - * @param string $prefix an optional string to display before the progress bar. - * Defaults to null meaning the prefix specified by [[startProgress()]] will be used. - * If prefix is specified it will update the prefix that will be used by later calls. - * @see startProgress - * @see endProgress - */ - public static function updateProgress($done, $total, $prefix = null) - { - $width = self::$_progressWidth; - if ($width === false) { - $width = 0; - } else { - $screenSize = static::getScreenSize(true); - if ($screenSize === false && $width < 1) { - $width = 0; - } elseif ($width === null) { - $width = $screenSize[0]; - } elseif ($width > 0 && $width < 1) { - $width = floor($screenSize[0] * $width); - } - } - if ($prefix === null) { - $prefix = self::$_progressPrefix; - } else { - self::$_progressPrefix = $prefix; - } - $width -= mb_strlen($prefix); - - $percent = ($total == 0) ? 1 : $done / $total; - $info = sprintf("%d%% (%d/%d)", $percent * 100, $done, $total); - - if ($done > $total || $done == 0) { - $info .= ' ETA: n/a'; - } elseif ($done < $total) { - $rate = (time() - self::$_progressStart) / $done; - $info .= sprintf(' ETA: %d sec.', $rate * ($total - $done)); - } - - $width -= 3 + mb_strlen($info); - // skipping progress bar on very small display or if forced to skip - if ($width < 5) { - static::stdout("\r$prefix$info "); - } else { - if ($percent < 0) { - $percent = 0; - } elseif ($percent > 1) { - $percent = 1; - } - $bar = floor($percent * $width); - $status = str_repeat("=", $bar); - if ($bar < $width) { - $status .= ">"; - $status .= str_repeat(" ", $width - $bar - 1); - } - static::stdout("\r$prefix" . "[$status] $info"); - } - flush(); - } - - /** - * Ends a progress bar that has been started by [[startProgress()]]. - * - * @param string|boolean $remove This can be `false` to leave the progress bar on screen and just print a newline. - * If set to `true`, the line of the progress bar will be cleared. This may also be a string to be displayed instead - * of the progress bar. - * @param boolean $keepPrefix whether to keep the prefix that has been specified for the progressbar when progressbar - * gets removed. Defaults to true. - * @see startProgress - * @see updateProgress - */ - public static function endProgress($remove = false, $keepPrefix = true) - { - if ($remove === false) { - static::stdout(PHP_EOL); - } else { - if (static::streamSupportsAnsiColors(STDOUT)) { - static::clearLine(); - } - static::stdout("\r" . ($keepPrefix ? self::$_progressPrefix : '') . (is_string($remove) ? $remove : '')); - } - flush(); - - self::$_progressStart = null; - self::$_progressWidth = null; - self::$_progressPrefix = ''; - } + const FG_BLACK = 30; + const FG_RED = 31; + const FG_GREEN = 32; + const FG_YELLOW = 33; + const FG_BLUE = 34; + const FG_PURPLE = 35; + const FG_CYAN = 36; + const FG_GREY = 37; + + const BG_BLACK = 40; + const BG_RED = 41; + const BG_GREEN = 42; + const BG_YELLOW = 43; + const BG_BLUE = 44; + const BG_PURPLE = 45; + const BG_CYAN = 46; + const BG_GREY = 47; + + const RESET = 0; + const NORMAL = 0; + const BOLD = 1; + const ITALIC = 3; + const UNDERLINE = 4; + const BLINK = 5; + const NEGATIVE = 7; + const CONCEALED = 8; + const CROSSED_OUT = 9; + const FRAMED = 51; + const ENCIRCLED = 52; + const OVERLINED = 53; + + /** + * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public static function moveCursorUp($rows = 1) + { + echo "\033[" . (int) $rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public static function moveCursorDown($rows = 1) + { + echo "\033[" . (int) $rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public static function moveCursorForward($steps = 1) + { + echo "\033[" . (int) $steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public static function moveCursorBackward($steps = 1) + { + echo "\033[" . (int) $steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public static function moveCursorNextLine($lines = 1) + { + echo "\033[" . (int) $lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public static function moveCursorPrevLine($lines = 1) + { + echo "\033[" . (int) $lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public static function moveCursorTo($column, $row = null) + { + if ($row === null) { + echo "\033[" . (int) $column . 'G'; + } else { + echo "\033[" . (int) $row . ';' . (int) $column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI control code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. + * @param integer $lines number of lines to scroll up + */ + public static function scrollUp($lines = 1) + { + echo "\033[" . (int) $lines . "S"; + } + + /** + * Scrolls whole page down by sending ANSI control code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS used in windows. + * @param integer $lines number of lines to scroll down + */ + public static function scrollDown($lines = 1) + { + echo "\033[" . (int) $lines . "T"; + } + + /** + * Saves the current cursor position by sending ANSI control code SCP to the terminal. + * Position can then be restored with [[restoreCursorPosition()]]. + */ + public static function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with [[saveCursorPosition()]] by sending ANSI control code RCP to the terminal. + */ + public static function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. + * Use [[showCursor()]] to bring it back. + * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. + */ + public static function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by [[hideCursor()]] by sending ANSI DECTCEM code ?25h to the terminal. + */ + public static function showCursor() + { + echo "\033[?25h"; + } + + /** + * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. + * Cursor position will not be changed. + * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. + */ + public static function clearScreen() + { + echo "\033[2J"; + } + + /** + * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + /** + * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLine() + { + echo "\033[2K"; + } + + /** + * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineAfterCursor() + { + echo "\033[0K"; + } + + /** + * Returns the ANSI format code. + * + * @param array $format An array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants + * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. + * @return string The ANSI format code according to the given formatting constants. + */ + public static function ansiFormatCode($format) + { + return "\033[" . implode(';', $format) . 'm'; + } + + /** + * Echoes an ANSI format code that affects the formatting of any text that is printed afterwards. + * + * @param array $format An array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants + * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. + * @see ansiFormatCode() + * @see endAnsiFormat() + */ + public static function beginAnsiFormat($format) + { + echo "\033[" . implode(';', $format) . 'm'; + } + + /** + * Resets any ANSI format set by previous method [[beginAnsiFormat()]] + * Any output after this will have default text format. + * This is equal to calling + * + * ```php + * echo Console::ansiFormatCode([Console::RESET]) + * ``` + */ + public static function endAnsiFormat() + { + echo "\033[0m"; + } + + /** + * Will return a string formatted with the given ANSI style + * + * @param string $string the string to be formatted + * @param array $format An array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants + * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format. + * @return string + */ + public static function ansiFormat($string, $format = []) + { + $code = implode(';', $format); + + return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string . "\033[0m"; + } + + /** + * Returns the ansi format code for xterm foreground color. + * You can pass the return value of this to one of the formatting methods: + * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]] + * + * @param integer $colorCode xterm color code + * @return string + * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + */ + public static function xtermFgColor($colorCode) + { + return '38;5;' . $colorCode; + } + + /** + * Returns the ansi format code for xterm background color. + * You can pass the return value of this to one of the formatting methods: + * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]] + * + * @param integer $colorCode xterm color code + * @return string + * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + */ + public static function xtermBgColor($colorCode) + { + return '48;5;' . $colorCode; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function stripAnsiFormat($string) + { + return preg_replace('/\033\[[\d;?]*\w/', '', $string); + } + + /** + * Converts an ANSI formatted string to HTML + * @param $string + * @return mixed + */ + // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746 + public static function ansiToHtml($string) + { + $tags = 0; + + return preg_replace_callback( + '/\033\[[\d;]+m/', + function ($ansi) use (&$tags) { + $styleA = []; + foreach (explode(';', $ansi) as $controlCode) { + switch ($controlCode) { + case self::FG_BLACK: + $style = ['color' => '#000000']; + break; + case self::FG_BLUE: + $style = ['color' => '#000078']; + break; + case self::FG_CYAN: + $style = ['color' => '#007878']; + break; + case self::FG_GREEN: + $style = ['color' => '#007800']; + break; + case self::FG_GREY: + $style = ['color' => '#787878']; + break; + case self::FG_PURPLE: + $style = ['color' => '#780078']; + break; + case self::FG_RED: + $style = ['color' => '#780000']; + break; + case self::FG_YELLOW: + $style = ['color' => '#787800']; + break; + case self::BG_BLACK: + $style = ['background-color' => '#000000']; + break; + case self::BG_BLUE: + $style = ['background-color' => '#000078']; + break; + case self::BG_CYAN: + $style = ['background-color' => '#007878']; + break; + case self::BG_GREEN: + $style = ['background-color' => '#007800']; + break; + case self::BG_GREY: + $style = ['background-color' => '#787878']; + break; + case self::BG_PURPLE: + $style = ['background-color' => '#780078']; + break; + case self::BG_RED: + $style = ['background-color' => '#780000']; + break; + case self::BG_YELLOW: + $style = ['background-color' => '#787800']; + break; + case self::BOLD: + $style = ['font-weight' => 'bold']; + break; + case self::ITALIC: + $style = ['font-style' => 'italic']; + break; + case self::UNDERLINE: + $style = ['text-decoration' => ['underline']]; + break; + case self::OVERLINED: + $style = ['text-decoration' => ['overline']]; + break; + case self::CROSSED_OUT: + $style = ['text-decoration' => ['line-through']]; + break; + case self::BLINK: + $style = ['text-decoration' => ['blink']]; + break; + case self::NEGATIVE: // ??? + case self::CONCEALED: + case self::ENCIRCLED: + case self::FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for (; $tags > 0; $tags--) { + $return .= ''; + } + + return $return; + } + + $styleA = ArrayHelper::merge($styleA, $style); + } + $styleString = []; + foreach ($styleA as $name => $content) { + if ($name === 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name . ':' . $content; + } + $tags++; + + return ' $value) { + static::output(" $key - $value"); + } + static::output(" ? - Show help"); + goto top; + } elseif (!in_array($input, array_keys($options))) { + goto top; + } + + return $input; + } + + private static $_progressStart; + private static $_progressWidth; + private static $_progressPrefix; + + /** + * Starts display of a progress bar on screen. + * + * This bar will be updated by [[updateProgress()]] and my be ended by [[endProgress()]]. + * + * The following example shows a simple usage of a progress bar: + * + * ```php + * Console::startProgress(0, 1000); + * for ($n = 1; $n <= 1000; $n++) { + * usleep(1000); + * Console::updateProgress($n, 1000); + * } + * Console::endProgress(); + * ``` + * + * Git clone like progress (showing only status information): + * ```php + * Console::startProgress(0, 1000, 'Counting objects: ', false); + * for ($n = 1; $n <= 1000; $n++) { + * usleep(1000); + * Console::updateProgress($n, 1000); + * } + * Console::endProgress("done." . PHP_EOL); + * ``` + * + * @param integer $done the number of items that are completed. + * @param integer $total the total value of items that are to be done. + * @param string $prefix an optional string to display before the progress bar. + * Default to empty string which results in no prefix to be displayed. + * @param integer|boolean $width optional width of the progressbar. This can be an integer representing + * the number of characters to display for the progress bar or a float between 0 and 1 representing the + * percentage of screen with the progress bar may take. It can also be set to false to disable the + * bar and only show progress information like percent, number of items and ETA. + * If not set, the bar will be as wide as the screen. Screen size will be detected using [[getScreenSize()]]. + * @see startProgress + * @see updateProgress + * @see endProgress + */ + public static function startProgress($done, $total, $prefix = '', $width = null) + { + self::$_progressStart = time(); + self::$_progressWidth = $width; + self::$_progressPrefix = $prefix; + + static::updateProgress($done, $total); + } + + /** + * Updates a progress bar that has been started by [[startProgress()]]. + * + * @param integer $done the number of items that are completed. + * @param integer $total the total value of items that are to be done. + * @param string $prefix an optional string to display before the progress bar. + * Defaults to null meaning the prefix specified by [[startProgress()]] will be used. + * If prefix is specified it will update the prefix that will be used by later calls. + * @see startProgress + * @see endProgress + */ + public static function updateProgress($done, $total, $prefix = null) + { + $width = self::$_progressWidth; + if ($width === false) { + $width = 0; + } else { + $screenSize = static::getScreenSize(true); + if ($screenSize === false && $width < 1) { + $width = 0; + } elseif ($width === null) { + $width = $screenSize[0]; + } elseif ($width > 0 && $width < 1) { + $width = floor($screenSize[0] * $width); + } + } + if ($prefix === null) { + $prefix = self::$_progressPrefix; + } else { + self::$_progressPrefix = $prefix; + } + $width -= mb_strlen($prefix); + + $percent = ($total == 0) ? 1 : $done / $total; + $info = sprintf("%d%% (%d/%d)", $percent * 100, $done, $total); + + if ($done > $total || $done == 0) { + $info .= ' ETA: n/a'; + } elseif ($done < $total) { + $rate = (time() - self::$_progressStart) / $done; + $info .= sprintf(' ETA: %d sec.', $rate * ($total - $done)); + } + + $width -= 3 + mb_strlen($info); + // skipping progress bar on very small display or if forced to skip + if ($width < 5) { + static::stdout("\r$prefix$info "); + } else { + if ($percent < 0) { + $percent = 0; + } elseif ($percent > 1) { + $percent = 1; + } + $bar = floor($percent * $width); + $status = str_repeat("=", $bar); + if ($bar < $width) { + $status .= ">"; + $status .= str_repeat(" ", $width - $bar - 1); + } + static::stdout("\r$prefix" . "[$status] $info"); + } + flush(); + } + + /** + * Ends a progress bar that has been started by [[startProgress()]]. + * + * @param string|boolean $remove This can be `false` to leave the progress bar on screen and just print a newline. + * If set to `true`, the line of the progress bar will be cleared. This may also be a string to be displayed instead + * of the progress bar. + * @param boolean $keepPrefix whether to keep the prefix that has been specified for the progressbar when progressbar + * gets removed. Defaults to true. + * @see startProgress + * @see updateProgress + */ + public static function endProgress($remove = false, $keepPrefix = true) + { + if ($remove === false) { + static::stdout(PHP_EOL); + } else { + if (static::streamSupportsAnsiColors(STDOUT)) { + static::clearLine(); + } + static::stdout("\r" . ($keepPrefix ? self::$_progressPrefix : '') . (is_string($remove) ? $remove : '')); + } + flush(); + + self::$_progressStart = null; + self::$_progressWidth = null; + self::$_progressPrefix = ''; + } } diff --git a/framework/helpers/BaseFileHelper.php b/framework/helpers/BaseFileHelper.php index 52448bf2cdd..25ab9aa3976 100644 --- a/framework/helpers/BaseFileHelper.php +++ b/framework/helpers/BaseFileHelper.php @@ -23,534 +23,545 @@ */ class BaseFileHelper { - const PATTERN_NODIR = 1; - const PATTERN_ENDSWITH = 4; - const PATTERN_MUSTBEDIR = 8; - const PATTERN_NEGATIVE = 16; - - /** - * Normalizes a file/directory path. - * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`, - * and any trailing directory separators will be removed. For example, '/home\demo/' on Linux - * will be normalized as '/home/demo'. - * @param string $path the file/directory path to be normalized - * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`. - * @return string the normalized file/directory path - */ - public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR) - { - return rtrim(strtr($path, ['/' => $ds, '\\' => $ds]), $ds); - } - - /** - * Returns the localized version of a specified file. - * - * The searching is based on the specified language code. In particular, - * a file with the same name will be looked for under the subdirectory - * whose name is the same as the language code. For example, given the file "path/to/view.php" - * and language code "zh-CN", the localized file will be looked for as - * "path/to/zh-CN/view.php". If the file is not found, it will try a fallback with just a language code that is - * "zh" i.e. "path/to/zh/view.php". If it is not found as well the original file will be returned. - * - * If the target and the source language codes are the same, - * the original file will be returned. - * - * @param string $file the original file - * @param string $language the target language that the file should be localized to. - * If not set, the value of [[\yii\base\Application::language]] will be used. - * @param string $sourceLanguage the language that the original file is in. - * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. - * @return string the matching localized file, or the original file if the localized version is not found. - * If the target and the source language codes are the same, the original file will be returned. - */ - public static function localize($file, $language = null, $sourceLanguage = null) - { - if ($language === null) { - $language = Yii::$app->language; - } - if ($sourceLanguage === null) { - $sourceLanguage = Yii::$app->sourceLanguage; - } - if ($language === $sourceLanguage) { - return $file; - } - $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); - if (is_file($desiredFile)) { - return $desiredFile; - } else { - $language = substr($language, 0, 2); - if ($language === $sourceLanguage) { - return $file; - } - $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); - return is_file($desiredFile) ? $desiredFile : $file; - } - } - - /** - * Determines the MIME type of the specified file. - * This method will first try to determine the MIME type based on - * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will - * fall back to [[getMimeTypeByExtension()]]. - * @param string $file the file name. - * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. - * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). - * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case - * `finfo_open()` cannot determine it. - * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. - */ - public static function getMimeType($file, $magicFile = null, $checkExtension = true) - { - if (function_exists('finfo_open')) { - $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); - if ($info) { - $result = finfo_file($info, $file); - finfo_close($info); - if ($result !== false) { - return $result; - } - } - } - - return $checkExtension ? static::getMimeTypeByExtension($file) : null; - } - - /** - * Determines the MIME type based on the extension name of the specified file. - * This method will use a local map between extension names and MIME types. - * @param string $file the file name. - * @param string $magicFile the path of the file that contains all available MIME type information. - * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. - * @return string the MIME type. Null is returned if the MIME type cannot be determined. - */ - public static function getMimeTypeByExtension($file, $magicFile = null) - { - static $mimeTypes = []; - if ($magicFile === null) { - $magicFile = __DIR__ . '/mimeTypes.php'; - } - if (!isset($mimeTypes[$magicFile])) { - $mimeTypes[$magicFile] = require($magicFile); - } - if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { - $ext = strtolower($ext); - if (isset($mimeTypes[$magicFile][$ext])) { - return $mimeTypes[$magicFile][$ext]; - } - } - return null; - } - - /** - * Copies a whole directory as another one. - * The files and sub-directories will also be copied over. - * @param string $src the source directory - * @param string $dst the destination directory - * @param array $options options for directory copy. Valid options are: - * - * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775. - * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. - * - filter: callback, a PHP callback that is called for each directory or file. - * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. - * The callback can return one of the following values: - * - * * true: the directory or file will be copied (the "only" and "except" options will be ignored) - * * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored) - * * null: the "only" and "except" options will determine whether the directory or file should be copied - * - * - only: array, list of patterns that the file paths should match if they want to be copied. - * A path matches a pattern if it contains the pattern string at its end. - * For example, '.php' matches all file paths ending with '.php'. - * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. - * If a file path matches a pattern in both "only" and "except", it will NOT be copied. - * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied. - * A path matches a pattern if it contains the pattern string at its end. - * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' - * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; - * and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches - * both '/' and '\' in the paths. - * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true. - * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. - * If the callback returns false, the copy operation for the sub-directory or file will be cancelled. - * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or - * file to be copied from, while `$to` is the copy target. - * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied. - * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or - * file copied from, while `$to` is the copy target. - * @throws \yii\base\InvalidParamException if unable to open directory - */ - public static function copyDirectory($src, $dst, $options = []) - { - if (!is_dir($dst)) { - static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true); - } - - $handle = opendir($src); - if ($handle === false) { - throw new InvalidParamException('Unable to open directory: ' . $src); - } - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $from = $src . DIRECTORY_SEPARATOR . $file; - $to = $dst . DIRECTORY_SEPARATOR . $file; - if (static::filterPath($from, $options)) { - if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) { - continue; - } - if (is_file($from)) { - copy($from, $to); - if (isset($options['fileMode'])) { - @chmod($to, $options['fileMode']); - } - } else { - static::copyDirectory($from, $to, $options); - } - if (isset($options['afterCopy'])) { - call_user_func($options['afterCopy'], $from, $to); - } - } - } - closedir($handle); - } - - /** - * Removes a directory (and all its content) recursively. - * @param string $dir the directory to be deleted recursively. - */ - public static function removeDirectory($dir) - { - if (!is_dir($dir) || !($handle = opendir($dir))) { - return; - } - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $path = $dir . DIRECTORY_SEPARATOR . $file; - if (is_file($path)) { - unlink($path); - } else { - static::removeDirectory($path); - } - } - closedir($handle); - rmdir($dir); - } - - /** - * Returns the files found under the specified directory and subdirectories. - * @param string $dir the directory under which the files will be looked for. - * @param array $options options for file searching. Valid options are: - * - * - filter: callback, a PHP callback that is called for each directory or file. - * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. - * The callback can return one of the following values: - * - * * true: the directory or file will be returned (the "only" and "except" options will be ignored) - * * false: the directory or file will NOT be returned (the "only" and "except" options will be ignored) - * * null: the "only" and "except" options will determine whether the directory or file should be returned - * - * - except: array, list of patterns excluding from the results matching file or directory paths. - * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' - * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; - * and '.svn/' matches directory paths ending with '.svn'. - * If the pattern does not contain a slash /, it is treated as a shell glob pattern and checked for a match against the pathname relative to $dir. - * Otherwise, the pattern is treated as a shell glob suitable for consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will not match a / in the pathname. - * For example, "views/*.php" matches "views/index.php" but not "views/controller/index.php". - * A leading slash matches the beginning of the pathname. For example, "/*.php" matches "index.php" but not "views/start/index.php". - * An optional prefix "!" which negates the pattern; any matching file excluded by a previous pattern will become included again. - * If a negated pattern matches, this will override lower precedence patterns sources. Put a backslash ("\") in front of the first "!" - * for patterns that begin with a literal "!", for example, "\!important!.txt". - * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. - * - only: array, list of patterns that the file paths should match if they are to be returned. Directory paths are not checked against them. - * Same pattern matching rules as in the "except" option are used. - * If a file path matches a pattern in both "only" and "except", it will NOT be returned. - * - recursive: boolean, whether the files under the subdirectories should also be looked for. Defaults to true. - * @return array files found under the directory. The file list is sorted. - * @throws InvalidParamException if the dir is invalid. - */ - public static function findFiles($dir, $options = []) - { - if (!is_dir($dir)) { - throw new InvalidParamException('The dir argument must be a directory.'); - } - $dir = rtrim($dir, DIRECTORY_SEPARATOR); - if (!isset($options['basePath'])) { - $options['basePath'] = realpath($dir); - // this should also be done only once - if (isset($options['except'])) { - foreach ($options['except'] as $key => $value) { - if (is_string($value)) { - $options['except'][$key] = static::parseExcludePattern($value); - } - } - } - if (isset($options['only'])) { - foreach ($options['only'] as $key => $value) { - if (is_string($value)) { - $options['only'][$key] = static::parseExcludePattern($value); - } - } - } - } - $list = []; - $handle = opendir($dir); - if ($handle === false) { - throw new InvalidParamException('Unable to open directory: ' . $dir); - } - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $path = $dir . DIRECTORY_SEPARATOR . $file; - if (static::filterPath($path, $options)) { - if (is_file($path)) { - $list[] = $path; - } elseif (!isset($options['recursive']) || $options['recursive']) { - $list = array_merge($list, static::findFiles($path, $options)); - } - } - } - closedir($handle); - return $list; - } - - /** - * Checks if the given file path satisfies the filtering options. - * @param string $path the path of the file or directory to be checked - * @param array $options the filtering options. See [[findFiles()]] for explanations of - * the supported options. - * @return boolean whether the file or directory satisfies the filtering options. - */ - public static function filterPath($path, $options) - { - if (isset($options['filter'])) { - $result = call_user_func($options['filter'], $path); - if (is_bool($result)) { - return $result; - } - } - - if (empty($options['except']) && empty($options['only'])) { - return true; - } - - $path = str_replace('\\', '/', $path); - - if (!empty($options['except'])) { - if (($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null) { - return $except['flags'] & self::PATTERN_NEGATIVE; - } - } - - if (!is_dir($path) && !empty($options['only'])) { - if (($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only'])) !== null) { - // don't check PATTERN_NEGATIVE since those entries are not prefixed with ! - return true; - } - return false; - } - return true; - } - - /** - * Creates a new directory. - * - * This method is similar to the PHP `mkdir()` function except that - * it uses `chmod()` to set the permission of the created directory - * in order to avoid the impact of the `umask` setting. - * - * @param string $path path of the directory to be created. - * @param integer $mode the permission to be set for the created directory. - * @param boolean $recursive whether to create parent directories if they do not exist. - * @return boolean whether the directory is created successfully - */ - public static function createDirectory($path, $mode = 0775, $recursive = true) - { - if (is_dir($path)) { - return true; - } - $parentDir = dirname($path); - if ($recursive && !is_dir($parentDir)) { - static::createDirectory($parentDir, $mode, true); - } - $result = mkdir($path, $mode); - chmod($path, $mode); - return $result; - } - - /** - * Performs a simple comparison of file or directory names. - * - * Based on match_basename() from dir.c of git 1.8.5.3 sources. - * - * @param string $baseName file or directory name to compare with the pattern - * @param string $pattern the pattern that $baseName will be compared against - * @param integer|boolean $firstWildcard location of first wildcard character in the $pattern - * @param integer $flags pattern flags - * @return boolean wheter the name matches against pattern - */ - private static function matchBasename($baseName, $pattern, $firstWildcard, $flags) - { - if ($firstWildcard === false) { - if ($pattern === $baseName) { - return true; - } - } elseif ($flags & self::PATTERN_ENDSWITH) { - /* "*literal" matching against "fooliteral" */ - $n = StringHelper::byteLength($pattern); - if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) { - return true; - } - } - return fnmatch($pattern, $baseName, 0); - } - - /** - * Compares a path part against a pattern with optional wildcards. - * - * Based on match_pathname() from dir.c of git 1.8.5.3 sources. - * - * @param string $path full path to compare - * @param string $basePath base of path that will not be compared - * @param string $pattern the pattern that path part will be compared against - * @param integer|boolean $firstWildcard location of first wildcard character in the $pattern - * @param integer $flags pattern flags - * @return boolean wheter the path part matches against pattern - */ - private static function matchPathname($path, $basePath, $pattern, $firstWildcard, $flags) - { - // match with FNM_PATHNAME; the pattern has base implicitly in front of it. - if (isset($pattern[0]) && $pattern[0] == '/') { - $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); - if ($firstWildcard !== false && $firstWildcard !== 0) { - $firstWildcard--; - } - } - - $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1); - $name = StringHelper::byteSubstr($path, -$namelen, $namelen); - - if ($firstWildcard !== 0) { - if ($firstWildcard === false) { - $firstWildcard = StringHelper::byteLength($pattern); - } - // if the non-wildcard part is longer than the remaining pathname, surely it cannot match. - if ($firstWildcard > $namelen) { - return false; - } - - if (strncmp($pattern, $name, $firstWildcard)) { - return false; - } - $pattern = StringHelper::byteSubstr($pattern, $firstWildcard, StringHelper::byteLength($pattern)); - $name = StringHelper::byteSubstr($name, $firstWildcard, $namelen); - - // If the whole pattern did not have a wildcard, then our prefix match is all we need; we do not need to call fnmatch at all. - if (empty($pattern) && empty($name)) { - return true; - } - } - - return fnmatch($pattern, $name, FNM_PATHNAME); - } - - /** - * Scan the given exclude list in reverse to see whether pathname - * should be ignored. The first match (i.e. the last on the list), if - * any, determines the fate. Returns the element which - * matched, or null for undecided. - * - * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources. - * - * @param string $basePath - * @param string $path - * @param array $excludes list of patterns to match $path against - * @return string null or one of $excludes item as an array with keys: 'pattern', 'flags' - * @throws InvalidParamException if any of the exclude patterns is not a string or an array with keys: pattern, flags, firstWildcard. - */ - private static function lastExcludeMatchingFromList($basePath, $path, $excludes) - { - foreach (array_reverse($excludes) as $exclude) { - if (is_string($exclude)) { - $exclude = self::parseExcludePattern($exclude); - } - if (!isset($exclude['pattern']) || !isset($exclude['flags']) || !isset($exclude['firstWildcard'])) { - throw new InvalidParamException('If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.'); - } - if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) { - continue; - } - - if ($exclude['flags'] & self::PATTERN_NODIR) { - if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { - return $exclude; - } - continue; - } - - if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { - return $exclude; - } - } - return null; - } - - /** - * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead. - * @param string $pattern - * @return array with keys: (string)pattern, (int)flags, (int|boolean)firstWildcard - * @throws InvalidParamException if the pattern is not a string. - */ - private static function parseExcludePattern($pattern) - { - if (!is_string($pattern)) { - throw new InvalidParamException('Exclude/include pattern must be a string.'); - } - $result = [ - 'pattern' => $pattern, - 'flags' => 0, - 'firstWildcard' => false, - ]; - if (!isset($pattern[0])) { - return $result; - } - - if ($pattern[0] == '!') { - $result['flags'] |= self::PATTERN_NEGATIVE; - $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); - } - $len = StringHelper::byteLength($pattern); - if ($len && StringHelper::byteSubstr($pattern, -1, 1) == '/') { - $pattern = StringHelper::byteSubstr($pattern, 0, -1); - $len--; - $result['flags'] |= self::PATTERN_MUSTBEDIR; - } - if (strpos($pattern, '/') === false) { - $result['flags'] |= self::PATTERN_NODIR; - } - $result['firstWildcard'] = self::firstWildcardInPattern($pattern); - if ($pattern[0] == '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { - $result['flags'] |= self::PATTERN_ENDSWITH; - } - $result['pattern'] = $pattern; - return $result; - } - - /** - * Searches for the first wildcard character in the pattern. - * @param string $pattern the pattern to search in - * @return integer|boolean position of first wildcard character or false if not found - */ - private static function firstWildcardInPattern($pattern) - { - $wildcards = ['*', '?', '[', '\\']; - $wildcardSearch = function ($r, $c) use ($pattern) { - $p = strpos($pattern, $c); - return $r===false ? $p : ($p===false ? $r : min($r, $p)); - }; - return array_reduce($wildcards, $wildcardSearch, false); - } + const PATTERN_NODIR = 1; + const PATTERN_ENDSWITH = 4; + const PATTERN_MUSTBEDIR = 8; + const PATTERN_NEGATIVE = 16; + + /** + * Normalizes a file/directory path. + * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`, + * and any trailing directory separators will be removed. For example, '/home\demo/' on Linux + * will be normalized as '/home/demo'. + * @param string $path the file/directory path to be normalized + * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`. + * @return string the normalized file/directory path + */ + public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR) + { + return rtrim(strtr($path, ['/' => $ds, '\\' => $ds]), $ds); + } + + /** + * Returns the localized version of a specified file. + * + * The searching is based on the specified language code. In particular, + * a file with the same name will be looked for under the subdirectory + * whose name is the same as the language code. For example, given the file "path/to/view.php" + * and language code "zh-CN", the localized file will be looked for as + * "path/to/zh-CN/view.php". If the file is not found, it will try a fallback with just a language code that is + * "zh" i.e. "path/to/zh/view.php". If it is not found as well the original file will be returned. + * + * If the target and the source language codes are the same, + * the original file will be returned. + * + * @param string $file the original file + * @param string $language the target language that the file should be localized to. + * If not set, the value of [[\yii\base\Application::language]] will be used. + * @param string $sourceLanguage the language that the original file is in. + * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. + * @return string the matching localized file, or the original file if the localized version is not found. + * If the target and the source language codes are the same, the original file will be returned. + */ + public static function localize($file, $language = null, $sourceLanguage = null) + { + if ($language === null) { + $language = Yii::$app->language; + } + if ($sourceLanguage === null) { + $sourceLanguage = Yii::$app->sourceLanguage; + } + if ($language === $sourceLanguage) { + return $file; + } + $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); + if (is_file($desiredFile)) { + return $desiredFile; + } else { + $language = substr($language, 0, 2); + if ($language === $sourceLanguage) { + return $file; + } + $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . basename($file); + + return is_file($desiredFile) ? $desiredFile : $file; + } + } + + /** + * Determines the MIME type of the specified file. + * This method will first try to determine the MIME type based on + * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will + * fall back to [[getMimeTypeByExtension()]]. + * @param string $file the file name. + * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. + * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). + * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case + * `finfo_open()` cannot determine it. + * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. + */ + public static function getMimeType($file, $magicFile = null, $checkExtension = true) + { + if (function_exists('finfo_open')) { + $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); + if ($info) { + $result = finfo_file($info, $file); + finfo_close($info); + if ($result !== false) { + return $result; + } + } + } + + return $checkExtension ? static::getMimeTypeByExtension($file) : null; + } + + /** + * Determines the MIME type based on the extension name of the specified file. + * This method will use a local map between extension names and MIME types. + * @param string $file the file name. + * @param string $magicFile the path of the file that contains all available MIME type information. + * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. + * @return string the MIME type. Null is returned if the MIME type cannot be determined. + */ + public static function getMimeTypeByExtension($file, $magicFile = null) + { + static $mimeTypes = []; + if ($magicFile === null) { + $magicFile = __DIR__ . '/mimeTypes.php'; + } + if (!isset($mimeTypes[$magicFile])) { + $mimeTypes[$magicFile] = require($magicFile); + } + if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { + $ext = strtolower($ext); + if (isset($mimeTypes[$magicFile][$ext])) { + return $mimeTypes[$magicFile][$ext]; + } + } + + return null; + } + + /** + * Copies a whole directory as another one. + * The files and sub-directories will also be copied over. + * @param string $src the source directory + * @param string $dst the destination directory + * @param array $options options for directory copy. Valid options are: + * + * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775. + * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. + * - filter: callback, a PHP callback that is called for each directory or file. + * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. + * The callback can return one of the following values: + * + * * true: the directory or file will be copied (the "only" and "except" options will be ignored) + * * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored) + * * null: the "only" and "except" options will determine whether the directory or file should be copied + * + * - only: array, list of patterns that the file paths should match if they want to be copied. + * A path matches a pattern if it contains the pattern string at its end. + * For example, '.php' matches all file paths ending with '.php'. + * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. + * If a file path matches a pattern in both "only" and "except", it will NOT be copied. + * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied. + * A path matches a pattern if it contains the pattern string at its end. + * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' + * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; + * and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches + * both '/' and '\' in the paths. + * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true. + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * If the callback returns false, the copy operation for the sub-directory or file will be cancelled. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file copied from, while `$to` is the copy target. + * @throws \yii\base\InvalidParamException if unable to open directory + */ + public static function copyDirectory($src, $dst, $options = []) + { + if (!is_dir($dst)) { + static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true); + } + + $handle = opendir($src); + if ($handle === false) { + throw new InvalidParamException('Unable to open directory: ' . $src); + } + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $from = $src . DIRECTORY_SEPARATOR . $file; + $to = $dst . DIRECTORY_SEPARATOR . $file; + if (static::filterPath($from, $options)) { + if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) { + continue; + } + if (is_file($from)) { + copy($from, $to); + if (isset($options['fileMode'])) { + @chmod($to, $options['fileMode']); + } + } else { + static::copyDirectory($from, $to, $options); + } + if (isset($options['afterCopy'])) { + call_user_func($options['afterCopy'], $from, $to); + } + } + } + closedir($handle); + } + + /** + * Removes a directory (and all its content) recursively. + * @param string $dir the directory to be deleted recursively. + */ + public static function removeDirectory($dir) + { + if (!is_dir($dir) || !($handle = opendir($dir))) { + return; + } + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_file($path)) { + unlink($path); + } else { + static::removeDirectory($path); + } + } + closedir($handle); + rmdir($dir); + } + + /** + * Returns the files found under the specified directory and subdirectories. + * @param string $dir the directory under which the files will be looked for. + * @param array $options options for file searching. Valid options are: + * + * - filter: callback, a PHP callback that is called for each directory or file. + * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered. + * The callback can return one of the following values: + * + * * true: the directory or file will be returned (the "only" and "except" options will be ignored) + * * false: the directory or file will NOT be returned (the "only" and "except" options will be ignored) + * * null: the "only" and "except" options will determine whether the directory or file should be returned + * + * - except: array, list of patterns excluding from the results matching file or directory paths. + * Patterns ending with '/' apply to directory paths only, and patterns not ending with '/' + * apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b'; + * and '.svn/' matches directory paths ending with '.svn'. + * If the pattern does not contain a slash /, it is treated as a shell glob pattern and checked for a match against the pathname relative to $dir. + * Otherwise, the pattern is treated as a shell glob suitable for consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will not match a / in the pathname. + * For example, "views/*.php" matches "views/index.php" but not "views/controller/index.php". + * A leading slash matches the beginning of the pathname. For example, "/*.php" matches "index.php" but not "views/start/index.php". + * An optional prefix "!" which negates the pattern; any matching file excluded by a previous pattern will become included again. + * If a negated pattern matches, this will override lower precedence patterns sources. Put a backslash ("\") in front of the first "!" + * for patterns that begin with a literal "!", for example, "\!important!.txt". + * Note, the '/' characters in a pattern matches both '/' and '\' in the paths. + * - only: array, list of patterns that the file paths should match if they are to be returned. Directory paths are not checked against them. + * Same pattern matching rules as in the "except" option are used. + * If a file path matches a pattern in both "only" and "except", it will NOT be returned. + * - recursive: boolean, whether the files under the subdirectories should also be looked for. Defaults to true. + * @return array files found under the directory. The file list is sorted. + * @throws InvalidParamException if the dir is invalid. + */ + public static function findFiles($dir, $options = []) + { + if (!is_dir($dir)) { + throw new InvalidParamException('The dir argument must be a directory.'); + } + $dir = rtrim($dir, DIRECTORY_SEPARATOR); + if (!isset($options['basePath'])) { + $options['basePath'] = realpath($dir); + // this should also be done only once + if (isset($options['except'])) { + foreach ($options['except'] as $key => $value) { + if (is_string($value)) { + $options['except'][$key] = static::parseExcludePattern($value); + } + } + } + if (isset($options['only'])) { + foreach ($options['only'] as $key => $value) { + if (is_string($value)) { + $options['only'][$key] = static::parseExcludePattern($value); + } + } + } + } + $list = []; + $handle = opendir($dir); + if ($handle === false) { + throw new InvalidParamException('Unable to open directory: ' . $dir); + } + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (static::filterPath($path, $options)) { + if (is_file($path)) { + $list[] = $path; + } elseif (!isset($options['recursive']) || $options['recursive']) { + $list = array_merge($list, static::findFiles($path, $options)); + } + } + } + closedir($handle); + + return $list; + } + + /** + * Checks if the given file path satisfies the filtering options. + * @param string $path the path of the file or directory to be checked + * @param array $options the filtering options. See [[findFiles()]] for explanations of + * the supported options. + * @return boolean whether the file or directory satisfies the filtering options. + */ + public static function filterPath($path, $options) + { + if (isset($options['filter'])) { + $result = call_user_func($options['filter'], $path); + if (is_bool($result)) { + return $result; + } + } + + if (empty($options['except']) && empty($options['only'])) { + return true; + } + + $path = str_replace('\\', '/', $path); + + if (!empty($options['except'])) { + if (($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null) { + return $except['flags'] & self::PATTERN_NEGATIVE; + } + } + + if (!is_dir($path) && !empty($options['only'])) { + if (($except = self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only'])) !== null) { + // don't check PATTERN_NEGATIVE since those entries are not prefixed with ! + return true; + } + + return false; + } + + return true; + } + + /** + * Creates a new directory. + * + * This method is similar to the PHP `mkdir()` function except that + * it uses `chmod()` to set the permission of the created directory + * in order to avoid the impact of the `umask` setting. + * + * @param string $path path of the directory to be created. + * @param integer $mode the permission to be set for the created directory. + * @param boolean $recursive whether to create parent directories if they do not exist. + * @return boolean whether the directory is created successfully + */ + public static function createDirectory($path, $mode = 0775, $recursive = true) + { + if (is_dir($path)) { + return true; + } + $parentDir = dirname($path); + if ($recursive && !is_dir($parentDir)) { + static::createDirectory($parentDir, $mode, true); + } + $result = mkdir($path, $mode); + chmod($path, $mode); + + return $result; + } + + /** + * Performs a simple comparison of file or directory names. + * + * Based on match_basename() from dir.c of git 1.8.5.3 sources. + * + * @param string $baseName file or directory name to compare with the pattern + * @param string $pattern the pattern that $baseName will be compared against + * @param integer|boolean $firstWildcard location of first wildcard character in the $pattern + * @param integer $flags pattern flags + * @return boolean wheter the name matches against pattern + */ + private static function matchBasename($baseName, $pattern, $firstWildcard, $flags) + { + if ($firstWildcard === false) { + if ($pattern === $baseName) { + return true; + } + } elseif ($flags & self::PATTERN_ENDSWITH) { + /* "*literal" matching against "fooliteral" */ + $n = StringHelper::byteLength($pattern); + if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) { + return true; + } + } + + return fnmatch($pattern, $baseName, 0); + } + + /** + * Compares a path part against a pattern with optional wildcards. + * + * Based on match_pathname() from dir.c of git 1.8.5.3 sources. + * + * @param string $path full path to compare + * @param string $basePath base of path that will not be compared + * @param string $pattern the pattern that path part will be compared against + * @param integer|boolean $firstWildcard location of first wildcard character in the $pattern + * @param integer $flags pattern flags + * @return boolean wheter the path part matches against pattern + */ + private static function matchPathname($path, $basePath, $pattern, $firstWildcard, $flags) + { + // match with FNM_PATHNAME; the pattern has base implicitly in front of it. + if (isset($pattern[0]) && $pattern[0] == '/') { + $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); + if ($firstWildcard !== false && $firstWildcard !== 0) { + $firstWildcard--; + } + } + + $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1); + $name = StringHelper::byteSubstr($path, -$namelen, $namelen); + + if ($firstWildcard !== 0) { + if ($firstWildcard === false) { + $firstWildcard = StringHelper::byteLength($pattern); + } + // if the non-wildcard part is longer than the remaining pathname, surely it cannot match. + if ($firstWildcard > $namelen) { + return false; + } + + if (strncmp($pattern, $name, $firstWildcard)) { + return false; + } + $pattern = StringHelper::byteSubstr($pattern, $firstWildcard, StringHelper::byteLength($pattern)); + $name = StringHelper::byteSubstr($name, $firstWildcard, $namelen); + + // If the whole pattern did not have a wildcard, then our prefix match is all we need; we do not need to call fnmatch at all. + if (empty($pattern) && empty($name)) { + return true; + } + } + + return fnmatch($pattern, $name, FNM_PATHNAME); + } + + /** + * Scan the given exclude list in reverse to see whether pathname + * should be ignored. The first match (i.e. the last on the list), if + * any, determines the fate. Returns the element which + * matched, or null for undecided. + * + * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources. + * + * @param string $basePath + * @param string $path + * @param array $excludes list of patterns to match $path against + * @return string null or one of $excludes item as an array with keys: 'pattern', 'flags' + * @throws InvalidParamException if any of the exclude patterns is not a string or an array with keys: pattern, flags, firstWildcard. + */ + private static function lastExcludeMatchingFromList($basePath, $path, $excludes) + { + foreach (array_reverse($excludes) as $exclude) { + if (is_string($exclude)) { + $exclude = self::parseExcludePattern($exclude); + } + if (!isset($exclude['pattern']) || !isset($exclude['flags']) || !isset($exclude['firstWildcard'])) { + throw new InvalidParamException('If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.'); + } + if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) { + continue; + } + + if ($exclude['flags'] & self::PATTERN_NODIR) { + if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { + return $exclude; + } + continue; + } + + if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) { + return $exclude; + } + } + + return null; + } + + /** + * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead. + * @param string $pattern + * @return array with keys: (string) pattern, (int) flags, (int|boolean)firstWildcard + * @throws InvalidParamException if the pattern is not a string. + */ + private static function parseExcludePattern($pattern) + { + if (!is_string($pattern)) { + throw new InvalidParamException('Exclude/include pattern must be a string.'); + } + $result = [ + 'pattern' => $pattern, + 'flags' => 0, + 'firstWildcard' => false, + ]; + if (!isset($pattern[0])) { + return $result; + } + + if ($pattern[0] == '!') { + $result['flags'] |= self::PATTERN_NEGATIVE; + $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern)); + } + $len = StringHelper::byteLength($pattern); + if ($len && StringHelper::byteSubstr($pattern, -1, 1) == '/') { + $pattern = StringHelper::byteSubstr($pattern, 0, -1); + $len--; + $result['flags'] |= self::PATTERN_MUSTBEDIR; + } + if (strpos($pattern, '/') === false) { + $result['flags'] |= self::PATTERN_NODIR; + } + $result['firstWildcard'] = self::firstWildcardInPattern($pattern); + if ($pattern[0] == '*' && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) { + $result['flags'] |= self::PATTERN_ENDSWITH; + } + $result['pattern'] = $pattern; + + return $result; + } + + /** + * Searches for the first wildcard character in the pattern. + * @param string $pattern the pattern to search in + * @return integer|boolean position of first wildcard character or false if not found + */ + private static function firstWildcardInPattern($pattern) + { + $wildcards = ['*', '?', '[', '\\']; + $wildcardSearch = function ($r, $c) use ($pattern) { + $p = strpos($pattern, $c); + + return $r===false ? $p : ($p===false ? $r : min($r, $p)); + }; + + return array_reduce($wildcards, $wildcardSearch, false); + } } diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php index 62ead216ba6..799857fa12d 100644 --- a/framework/helpers/BaseHtml.php +++ b/framework/helpers/BaseHtml.php @@ -23,1785 +23,1817 @@ */ class BaseHtml { - /** - * @var array list of void elements (element name => 1) - * @see http://www.w3.org/TR/html-markup/syntax.html#void-element - */ - public static $voidElements = [ - 'area' => 1, - 'base' => 1, - 'br' => 1, - 'col' => 1, - 'command' => 1, - 'embed' => 1, - 'hr' => 1, - 'img' => 1, - 'input' => 1, - 'keygen' => 1, - 'link' => 1, - 'meta' => 1, - 'param' => 1, - 'source' => 1, - 'track' => 1, - 'wbr' => 1, - ]; - /** - * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes - * that are rendered by [[renderTagAttributes()]]. - */ - public static $attributeOrder = [ - 'type', - 'id', - 'class', - 'name', - 'value', - - 'href', - 'src', - 'action', - 'method', - - 'selected', - 'checked', - 'readonly', - 'disabled', - 'multiple', - - 'size', - 'maxlength', - 'width', - 'height', - 'rows', - 'cols', - - 'alt', - 'title', - 'rel', - 'media', - ]; - - - /** - * Encodes special characters into HTML entities. - * The [[\yii\base\Application::charset|application charset]] will be used for encoding. - * @param string $content the content to be encoded - * @param boolean $doubleEncode whether to encode HTML entities in `$content`. If false, - * HTML entities in `$content` will not be further encoded. - * @return string the encoded content - * @see decode() - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function encode($content, $doubleEncode = true) - { - return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset, $doubleEncode); - } - - /** - * Decodes special HTML entities back to the corresponding characters. - * This is the opposite of [[encode()]]. - * @param string $content the content to be decoded - * @return string the decoded content - * @see encode() - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function decode($content) - { - return htmlspecialchars_decode($content, ENT_QUOTES); - } - - /** - * Generates a complete HTML tag. - * @param string $name the tag name - * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. - * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the HTML tag attributes (HTML options) in terms of name-value pairs. - * These will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * - * For example when using `['class' => 'my-class', 'target' => '_blank', 'value' => null]` it will result in the - * html attributes rendered like this: `class="my-class" target="_blank"`. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated HTML tag - * @see beginTag() - * @see endTag() - */ - public static function tag($name, $content = '', $options = []) - { - $html = "<$name" . static::renderTagAttributes($options) . '>'; - return isset(static::$voidElements[strtolower($name)]) ? $html : "$html$content"; - } - - /** - * Generates a start tag. - * @param string $name the tag name - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated start tag - * @see endTag() - * @see tag() - */ - public static function beginTag($name, $options = []) - { - return "<$name" . static::renderTagAttributes($options) . '>'; - } - - /** - * Generates an end tag. - * @param string $name the tag name - * @return string the generated end tag - * @see beginTag() - * @see tag() - */ - public static function endTag($name) - { - return ""; - } - - /** - * Generates a style tag. - * @param string $content the style content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/css" will be used. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated style tag - */ - public static function style($content, $options = []) - { - return static::tag('style', $content, $options); - } - - /** - * Generates a script tag. - * @param string $content the script content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated script tag - */ - public static function script($content, $options = []) - { - return static::tag('script', $content, $options); - } - - /** - * Generates a link tag that refers to an external CSS file. - * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[\yii\helpers\Url::to()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated link tag - * @see \yii\helpers\Url::to() - */ - public static function cssFile($url, $options = []) - { - if (!isset($options['rel'])) { - $options['rel'] = 'stylesheet'; - } - $options['href'] = Url::to($url); - return static::tag('link', '', $options); - } - - /** - * Generates a script tag that refers to an external JavaScript file. - * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[\yii\helpers\Url::to()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated script tag - * @see \yii\helpers\Url::to() - */ - public static function jsFile($url, $options = []) - { - $options['src'] = Url::to($url); - return static::tag('script', '', $options); - } - - /** - * Generates a form start tag. - * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Url::to()]]. - * @param string $method the form submission method, such as "post", "get", "put", "delete" (case-insensitive). - * Since most browsers only support "post" and "get", if other methods are given, they will - * be simulated using "post", and a hidden input will be added which contains the actual method type. - * See [[\yii\web\Request::methodParam]] for more details. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated form start tag. - * @see endForm() - */ - public static function beginForm($action = '', $method = 'post', $options = []) - { - $action = Url::to($action); - - $hiddenInputs = []; - - $request = Yii::$app->getRequest(); - if ($request instanceof Request) { - if (strcasecmp($method, 'get') && strcasecmp($method, 'post')) { - // simulate PUT, DELETE, etc. via POST - $hiddenInputs[] = static::hiddenInput($request->methodParam, $method); - $method = 'post'; - } - if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { - $hiddenInputs[] = static::hiddenInput($request->csrfParam, $request->getCsrfToken()); - } - } - - if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { - // query parameters in the action are ignored for GET method - // we use hidden fields to add them back - foreach (explode('&', substr($action, $pos + 1)) as $pair) { - if (($pos1 = strpos($pair, '=')) !== false) { - $hiddenInputs[] = static::hiddenInput( - urldecode(substr($pair, 0, $pos1)), - urldecode(substr($pair, $pos1 + 1)) - ); - } else { - $hiddenInputs[] = static::hiddenInput(urldecode($pair), ''); - } - } - $action = substr($action, 0, $pos); - } - - $options['action'] = $action; - $options['method'] = $method; - $form = static::beginTag('form', $options); - if (!empty($hiddenInputs)) { - $form .= "\n" . implode("\n", $hiddenInputs); - } - - return $form; - } - - /** - * Generates a form end tag. - * @return string the generated tag - * @see beginForm() - */ - public static function endForm() - { - return ''; - } - - /** - * Generates a hyperlink tag. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[yii\helpers\Url::to()]] - * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute - * will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated hyperlink - * @see yii\helpers\Url::to() - */ - public static function a($text, $url = null, $options = []) - { - if ($url !== null) { - $options['href'] = Url::to($url); - } - return static::tag('a', $text, $options); - } - - /** - * Generates a mailto hyperlink. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $email email address. If this is null, the first parameter (link body) will be treated - * as the email address and used. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated mailto link - */ - public static function mailto($text, $email = null, $options = []) - { - $options['href'] = 'mailto:' . ($email === null ? $text : $email); - return static::tag('a', $text, $options); - } - - /** - * Generates an image tag. - * @param string $src the image URL. This parameter will be processed by [[yii\helpers\Url::to()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated image tag - */ - public static function img($src, $options = []) - { - $options['src'] = Url::to($src); - if (!isset($options['alt'])) { - $options['alt'] = ''; - } - return static::tag('img', '', $options); - } - - /** - * Generates a label tag. - * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should [[encode()]] - * it to prevent XSS attacks. - * @param string $for the ID of the HTML element that this label is associated with. - * If this is null, the "for" attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated label tag - */ - public static function label($content, $for = null, $options = []) - { - $options['for'] = $for; - return static::tag('label', $content, $options); - } - - /** - * Generates a button tag. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function button($content = 'Button', $options = []) - { - return static::tag('button', $content, $options); - } - - /** - * Generates a submit button tag. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated submit button tag - */ - public static function submitButton($content = 'Submit', $options = []) - { - $options['type'] = 'submit'; - return static::button($content, $options); - } - - /** - * Generates a reset button tag. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated reset button tag - */ - public static function resetButton($content = 'Reset', $options = []) - { - $options['type'] = 'reset'; - return static::button($content, $options); - } - - /** - * Generates an input type of the given type. - * @param string $type the type attribute. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated input tag - */ - public static function input($type, $name = null, $value = null, $options = []) - { - $options['type'] = $type; - $options['name'] = $name; - $options['value'] = $value === null ? null : (string)$value; - return static::tag('input', '', $options); - } - - /** - * Generates an input button. - * @param string $label the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function buttonInput($label = 'Button', $options = []) - { - $options['type'] = 'button'; - $options['value'] = $label; - return static::tag('input', '', $options); - } - - /** - * Generates a submit input button. - * @param string $label the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function submitInput($label = 'Submit', $options = []) - { - $options['type'] = 'submit'; - $options['value'] = $label; - return static::tag('input', '', $options); - } - - /** - * Generates a reset input button. - * @param string $label the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function resetInput($label = 'Reset', $options = []) - { - $options['type'] = 'reset'; - $options['value'] = $label; - return static::tag('input', '', $options); - } - - /** - * Generates a text input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function textInput($name, $value = null, $options = []) - { - return static::input('text', $name, $value, $options); - } - - /** - * Generates a hidden input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function hiddenInput($name, $value = null, $options = []) - { - return static::input('hidden', $name, $value, $options); - } - - /** - * Generates a password input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function passwordInput($name, $value = null, $options = []) - { - return static::input('password', $name, $value, $options); - } - - /** - * Generates a file input field. - * To use a file input field, you should set the enclosing form's "enctype" attribute to - * be "multipart/form-data". After the form is submitted, the uploaded file information - * can be obtained via $_FILES[$name] (see PHP documentation). - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated button tag - */ - public static function fileInput($name, $value = null, $options = []) - { - return static::input('file', $name, $value, $options); - } - - /** - * Generates a text area input. - * @param string $name the input name - * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated text area tag - */ - public static function textarea($name, $value = '', $options = []) - { - $options['name'] = $name; - return static::tag('textarea', static::encode($value), $options); - } - - /** - * Generates a radio button input. - * @param string $name the name attribute. - * @param boolean $checked whether the radio button should be checked. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute - * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass - * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. - * When this option is specified, the radio button will be enclosed by a label tag. - * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. - * - container: array|boolean, the HTML attributes for the container tag. This is only used when the "label" option is specified. - * If it is false, no container will be rendered. If it is an array or not, a "div" container will be rendered - * around the the radio button. - * - * The rest of the options will be rendered as the attributes of the resulting radio button tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated radio button tag - */ - public static function radio($name, $checked = false, $options = []) - { - $options['checked'] = (boolean)$checked; - $value = array_key_exists('value', $options) ? $options['value'] : '1'; - if (isset($options['uncheck'])) { - // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - if (isset($options['label'])) { - $label = $options['label']; - $labelOptions = isset($options['labelOptions']) ? $options['labelOptions'] : []; - $container = isset($options['container']) ? $options['container'] : ['class' => 'radio']; - unset($options['label'], $options['labelOptions'], $options['container']); - $content = static::label(static::input('radio', $name, $value, $options) . ' ' . $label, null, $labelOptions); - if (is_array($container)) { - return $hidden . static::tag('div', $content, $container); - } else { - return $hidden . $content; - } - } else { - return $hidden . static::input('radio', $name, $value, $options); - } - } - - /** - * Generates a checkbox input. - * @param string $name the name attribute. - * @param boolean $checked whether the checkbox should be checked. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute - * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass - * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. - * When this option is specified, the checkbox will be enclosed by a label tag. - * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. - * - container: array|boolean, the HTML attributes for the container tag. This is only used when the "label" option is specified. - * If it is false, no container will be rendered. If it is an array or not, a "div" container will be rendered - * around the the radio button. - * - * The rest of the options will be rendered as the attributes of the resulting checkbox tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated checkbox tag - */ - public static function checkbox($name, $checked = false, $options = []) - { - $options['checked'] = (boolean)$checked; - $value = array_key_exists('value', $options) ? $options['value'] : '1'; - if (isset($options['uncheck'])) { - // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - if (isset($options['label'])) { - $label = $options['label']; - $labelOptions = isset($options['labelOptions']) ? $options['labelOptions'] : []; - $container = isset($options['container']) ? $options['container'] : ['class' => 'checkbox']; - unset($options['label'], $options['labelOptions'], $options['container']); - $content = static::label(static::input('checkbox', $name, $value, $options) . ' ' . $label, null, $labelOptions); - if (is_array($container)) { - return $hidden . static::tag('div', $content, $container); - } else { - return $hidden . $content; - } - } else { - return $hidden . static::input('checkbox', $name, $value, $options); - } - } - - /** - * Generates a drop-down list. - * @param string $name the input name - * @param string $selection the selected value - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated drop-down list tag - */ - public static function dropDownList($name, $selection = null, $items = [], $options = []) - { - if (!empty($options['multiple'])) { - return static::listBox($name, $selection, $items, $options); - } - $options['name'] = $name; - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list box. - * @param string $name the input name - * @param string|array $selection the selected value(s) - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated list box tag - */ - public static function listBox($name, $selection = null, $items = [], $options = []) - { - if (!array_key_exists('size', $options)) { - $options['size'] = 4; - } - if (!empty($options['multiple']) && substr($name, -2) !== '[]') { - $name .= '[]'; - } - $options['name'] = $name; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - if (substr($name, -2) === '[]') { - $name = substr($name, 0, -2); - } - $hidden = static::hiddenInput($name, $options['unselect']); - unset($options['unselect']); - } else { - $hidden = ''; - } - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list of checkboxes. - * A checkbox list allows multiple selection, like [[listBox()]]. - * As a result, the corresponding submitted value is an array. - * @param string $name the name attribute of each checkbox. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the checkboxes. - * The array values are the labels, while the array keys are the corresponding checkbox values. - * @param array $options options (name => config) for the checkbox list container tag. - * The following options are specially handled: - * - * - tag: string, the tag name of the container element. - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * By setting this option, a hidden input will be generated. - * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. - * This option is ignored if `item` option is set. - * - separator: string, the HTML code that separates items. - * - itemOptions: array, the options for generating the radio button tag using [[checkbox()]]. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input, respectively. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated checkbox list - */ - public static function checkboxList($name, $selection = null, $items = [], $options = []) - { - if (substr($name, -2) !== '[]') { - $name .= '[]'; - } - - $formatter = isset($options['item']) ? $options['item'] : null; - $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : []; - $encode = !isset($options['encode']) || $options['encode']; - $lines = []; - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::checkbox($name, $checked, array_merge($itemOptions, [ - 'value' => $value, - 'label' => $encode ? static::encode($label) : $label, - ])); - } - $index++; - } - - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; - $hidden = static::hiddenInput($name2, $options['unselect']); - } else { - $hidden = ''; - } - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - - $tag = isset($options['tag']) ? $options['tag'] : 'div'; - unset($options['tag'], $options['unselect'], $options['encode'], $options['separator'], $options['item'], $options['itemOptions']); - - return $hidden . static::tag($tag, implode($separator, $lines), $options); - } - - /** - * Generates a list of radio buttons. - * A radio button list is like a checkbox list, except that it only allows single selection. - * @param string $name the name attribute of each radio button. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the radio buttons. - * The array values are the labels, while the array keys are the corresponding radio button values. - * @param array $options options (name => config) for the radio button list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * By setting this option, a hidden input will be generated. - * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. - * This option is ignored if `item` option is set. - * - separator: string, the HTML code that separates items. - * - itemOptions: array, the options for generating the radio button tag using [[radio()]]. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input, respectively. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated radio button list - */ - public static function radioList($name, $selection = null, $items = [], $options = []) - { - $encode = !isset($options['encode']) || $options['encode']; - $formatter = isset($options['item']) ? $options['item'] : null; - $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : []; - $lines = []; - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::radio($name, $checked, array_merge($itemOptions, [ - 'value' => $value, - 'label' => $encode ? static::encode($label) : $label, - ])); - } - $index++; - } - - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $hidden = static::hiddenInput($name, $options['unselect']); - } else { - $hidden = ''; - } - - $tag = isset($options['tag']) ? $options['tag'] : 'div'; - unset($options['tag'], $options['unselect'], $options['encode'], $options['separator'], $options['item'], $options['itemOptions']); - - return $hidden . static::tag($tag, implode($separator, $lines), $options); - } - - /** - * Generates an unordered list. - * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. - * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. - * @param array $options options (name => config) for the radio button list. The following options are supported: - * - * - encode: boolean, whether to HTML-encode the items. Defaults to true. - * This option is ignored if the `item` option is specified. - * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. - * - item: callable, a callback that is used to generate each individual list item. - * The signature of this callback must be: - * - * ~~~ - * function ($item, $index) - * ~~~ - * - * where $index is the array key corresponding to `$item` in `$items`. The callback should return - * the whole list item tag. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated unordered list. An empty string is returned if `$items` is empty. - */ - public static function ul($items, $options = []) - { - if (empty($items)) { - return ''; - } - $tag = isset($options['tag']) ? $options['tag'] : 'ul'; - $encode = !isset($options['encode']) || $options['encode']; - $formatter = isset($options['item']) ? $options['item'] : null; - $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : []; - unset($options['tag'], $options['encode'], $options['item'], $options['itemOptions']); - $results = []; - foreach ($items as $index => $item) { - if ($formatter !== null) { - $results[] = call_user_func($formatter, $item, $index); - } else { - $results[] = static::tag('li', $encode ? static::encode($item) : $item, $itemOptions); - } - } - return static::tag($tag, "\n" . implode("\n", $results) . "\n", $options); - } - - /** - * Generates an ordered list. - * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. - * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. - * @param array $options options (name => config) for the radio button list. The following options are supported: - * - * - encode: boolean, whether to HTML-encode the items. Defaults to true. - * This option is ignored if the `item` option is specified. - * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. - * - item: callable, a callback that is used to generate each individual list item. - * The signature of this callback must be: - * - * ~~~ - * function ($item, $index) - * ~~~ - * - * where $index is the array key corresponding to `$item` in `$items`. The callback should return - * the whole list item tag. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated ordered list. An empty string is returned if `$items` is empty. - */ - public static function ol($items, $options = []) - { - $options['tag'] = 'ol'; - return static::ul($items, $options); - } - - /** - * Generates a label tag for the given model attribute. - * The label text is the label associated with the attribute, obtained via [[Model::getAttributeLabel()]]. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * The following options are specially handled: - * - * - label: this specifies the label to be displayed. Note that this will NOT be [[encode()|encoded]]. - * If this is not set, [[Model::getAttributeLabel()]] will be called to get the label for display - * (after encoding). - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated label tag - */ - public static function activeLabel($model, $attribute, $options = []) - { - $for = array_key_exists('for', $options) ? $options['for'] : static::getInputId($model, $attribute); - $attribute = static::getAttributeName($attribute); - $label = isset($options['label']) ? $options['label'] : static::encode($model->getAttributeLabel($attribute)); - unset($options['label'], $options['for']); - return static::label($label, $for, $options); - } - - /** - * Generates a tag that contains the first validation error of the specified model attribute. - * Note that even if there is no validation error, this method will still return an empty error tag. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. The values will be HTML-encoded - * using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * The following options are specially handled: - * - * - tag: this specifies the tag name. If not set, "div" will be used. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated label tag - */ - public static function error($model, $attribute, $options = []) - { - $attribute = static::getAttributeName($attribute); - $error = $model->getFirstError($attribute); - $tag = isset($options['tag']) ? $options['tag'] : 'div'; - unset($options['tag']); - return Html::tag($tag, Html::encode($error), $options); - } - - /** - * Generates an input tag for the given model attribute. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param string $type the input type (e.g. 'text', 'password') - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated input tag - */ - public static function activeInput($type, $model, $attribute, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $value = isset($options['value']) ? $options['value'] : static::getAttributeValue($model, $attribute); - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::input($type, $name, $value, $options); - } - - /** - * Generates a text input tag for the given model attribute. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated input tag - */ - public static function activeTextInput($model, $attribute, $options = []) - { - return static::activeInput('text', $model, $attribute, $options); - } - - /** - * Generates a hidden input tag for the given model attribute. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated input tag - */ - public static function activeHiddenInput($model, $attribute, $options = []) - { - return static::activeInput('hidden', $model, $attribute, $options); - } - - /** - * Generates a password input tag for the given model attribute. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated input tag - */ - public static function activePasswordInput($model, $attribute, $options = []) - { - return static::activeInput('password', $model, $attribute, $options); - } - - /** - * Generates a file input tag for the given model attribute. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated input tag - */ - public static function activeFileInput($model, $attribute, $options = []) - { - // add a hidden field so that if a model only has a file field, we can - // still use isset($_POST[$modelClass]) to detect if the input is submitted - return static::activeHiddenInput($model, $attribute, ['id' => null, 'value' => '']) - . static::activeInput('file', $model, $attribute, $options); - } - - /** - * Generates a textarea tag for the given model attribute. - * The model attribute value will be used as the content in the textarea. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * @return string the generated textarea tag - */ - public static function activeTextarea($model, $attribute, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $value = static::getAttributeValue($model, $attribute); - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::textarea($name, $value, $options); - } - - /** - * Generates a radio button tag for the given model attribute. - * This method will generate the "checked" tag attribute according to the model attribute value. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, - * it will take the default value '0'. This method will render a hidden input so that if the radio button - * is not checked and is submitted, the value of this attribute will still be submitted to the server - * via the hidden input. - * - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass - * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. - * When this option is specified, the radio button will be enclosed by a label tag. - * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated radio button tag - */ - public static function activeRadio($model, $attribute, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $value = static::getAttributeValue($model, $attribute); - - if (!array_key_exists('value', $options)) { - $options['value'] = '1'; - } - if (!array_key_exists('uncheck', $options)) { - $options['uncheck'] = '0'; - } - - $checked = "$value" === "{$options['value']}"; - - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::radio($name, $checked, $options); - } - - /** - * Generates a checkbox tag for the given model attribute. - * This method will generate the "checked" tag attribute according to the model attribute value. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, - * it will take the default value '0'. This method will render a hidden input so that if the radio button - * is not checked and is submitted, the value of this attribute will still be submitted to the server - * via the hidden input. - * - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass - * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. - * When this option is specified, the checkbox will be enclosed by a label tag. - * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated checkbox tag - */ - public static function activeCheckbox($model, $attribute, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $value = static::getAttributeValue($model, $attribute); - - if (!array_key_exists('value', $options)) { - $options['value'] = '1'; - } - if (!array_key_exists('uncheck', $options)) { - $options['uncheck'] = '0'; - } - - $checked = "$value" === "{$options['value']}"; - - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::checkbox($name, $checked, $options); - } - - /** - * Generates a drop-down list for the given model attribute. - * The selection of the drop-down list is taken from the value of the model attribute. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated drop-down list tag - */ - public static function activeDropDownList($model, $attribute, $items, $options = []) - { - if (!empty($options['multiple'])) { - return static::activeListBox($model, $attribute, $items, $options); - } - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $selection = static::getAttributeValue($model, $attribute); - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::dropDownList($name, $selection, $items, $options); - } - - /** - * Generates a list box. - * The selection of the list box is taken from the value of the model attribute. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated list box tag - */ - public static function activeListBox($model, $attribute, $items, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $selection = static::getAttributeValue($model, $attribute); - if (!array_key_exists('unselect', $options)) { - $options['unselect'] = ''; - } - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::listBox($name, $selection, $items, $options); - } - - /** - * Generates a list of checkboxes. - * A checkbox list allows multiple selection, like [[listBox()]]. - * As a result, the corresponding submitted value is an array. - * The selection of the checkbox list is taken from the value of the model attribute. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $items the data item used to generate the checkboxes. - * The array values are the labels, while the array keys are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are specially handled: - * - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * You may set this option to be null to prevent default value submission. - * If this option is not set, an empty string will be submitted. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated checkbox list - */ - public static function activeCheckboxList($model, $attribute, $items, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $selection = static::getAttributeValue($model, $attribute); - if (!array_key_exists('unselect', $options)) { - $options['unselect'] = ''; - } - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::checkboxList($name, $selection, $items, $options); - } - - /** - * Generates a list of radio buttons. - * A radio button list is like a checkbox list, except that it only allows single selection. - * The selection of the radio buttons is taken from the value of the model attribute. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format - * about attribute expression. - * @param array $items the data item used to generate the radio buttons. - * The array keys are the labels, while the array values are the corresponding radio button values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are specially handled: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * You may set this option to be null to prevent default value submission. - * If this option is not set, an empty string will be submitted. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input. - * - * See [[renderTagAttributes()]] for details on how attributes are being rendered. - * - * @return string the generated radio button list - */ - public static function activeRadioList($model, $attribute, $items, $options = []) - { - $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); - $selection = static::getAttributeValue($model, $attribute); - if (!array_key_exists('unselect', $options)) { - $options['unselect'] = ''; - } - if (!array_key_exists('id', $options)) { - $options['id'] = static::getInputId($model, $attribute); - } - return static::radioList($name, $selection, $items, $options); - } - - /** - * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. - * @param string|array $selection the selected value(s). This can be either a string for single selection - * or an array for multiple selections. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. - * This method will take out these elements, if any: "prompt", "options" and "groups". See more details - * in [[dropDownList()]] for the explanation of these elements. - * - * @return string the generated list options - */ - public static function renderSelectOptions($selection, $items, &$tagOptions = []) - { - $lines = []; - if (isset($tagOptions['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); - $lines[] = static::tag('option', $prompt, ['value' => '']); - } - - $options = isset($tagOptions['options']) ? $tagOptions['options'] : []; - $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : []; - unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); - - foreach ($items as $key => $value) { - if (is_array($value)) { - $groupAttrs = isset($groups[$key]) ? $groups[$key] : []; - $groupAttrs['label'] = $key; - $attrs = ['options' => $options, 'groups' => $groups]; - $content = static::renderSelectOptions($selection, $value, $attrs); - $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); - } else { - $attrs = isset($options[$key]) ? $options[$key] : []; - $attrs['value'] = (string)$key; - $attrs['selected'] = $selection !== null && - (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); - } - } - - return implode("\n", $lines); - } - - /** - * Renders the HTML tag attributes. - * - * Attributes whose values are of boolean type will be treated as - * [boolean attributes](http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes). - * - * Attributes whose values are null will not be rendered. - * - * The values of attributes will be HTML-encoded using [[encode()]]. - * - * The "data" attribute is specially handled when it is receiving an array value. In this case, - * the array will be "expanded" and a list data attributes will be rendered. For example, - * if `'data' => ['id' => 1, 'name' => 'yii']`, then this will be rendered: - * `data-id="1" data-name="yii"`. - * - * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. - * @return string the rendering result. If the attributes are not empty, they will be rendered - * into a string with a leading white space (so that it can be directly appended to the tag name - * in a tag. If there is no attribute, an empty string will be returned. - */ - public static function renderTagAttributes($attributes) - { - if (count($attributes) > 1) { - $sorted = []; - foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; - } - } - $attributes = array_merge($sorted, $attributes); - } - - $html = ''; - foreach ($attributes as $name => $value) { - if (is_bool($value)) { - if ($value) { - $html .= " $name"; - } - } elseif (is_array($value) && $name === 'data') { - foreach ($value as $n => $v) { - $html .= " $name-$n=\"" . static::encode($v) . '"'; - } - } elseif ($value !== null) { - $html .= " $name=\"" . static::encode($value) . '"'; - } - } - return $html; - } - - /** - * Adds a CSS class to the specified options. - * If the CSS class is already in the options, it will not be added again. - * @param array $options the options to be modified. - * @param string $class the CSS class to be added - */ - public static function addCssClass(&$options, $class) - { - if (isset($options['class'])) { - $classes = ' ' . $options['class'] . ' '; - if (strpos($classes, ' ' . $class . ' ') === false) { - $options['class'] .= ' ' . $class; - } - } else { - $options['class'] = $class; - } - } - - /** - * Removes a CSS class from the specified options. - * @param array $options the options to be modified. - * @param string $class the CSS class to be removed - */ - public static function removeCssClass(&$options, $class) - { - if (isset($options['class'])) { - $classes = array_unique(preg_split('/\s+/', $options['class'] . ' ' . $class, -1, PREG_SPLIT_NO_EMPTY)); - if (($index = array_search($class, $classes)) !== false) { - unset($classes[$index]); - } - if (empty($classes)) { - unset($options['class']); - } else { - $options['class'] = implode(' ', $classes); - } - } - } - - /** - * Adds the specified CSS style to the HTML options. - * - * If the options already contain a `style` element, the new style will be merged - * with the existing one. If a CSS property exists in both the new and the old styles, - * the old one may be overwritten if `$overwrite` is true. - * - * For example, - * - * ```php - * Html::addCssStyle($options, 'width: 100px; height: 200px'); - * ``` - * - * @param array $options the HTML options to be modified. - * @param string|array $style the new style string (e.g. `'width: 100px; height: 200px'`) or - * array (e.g. `['width' => '100px', 'height' => '200px']`). - * @param boolean $overwrite whether to overwrite existing CSS properties if the new style - * contain them too. - * @see removeCssStyle() - * @see cssStyleFromArray() - * @see cssStyleToArray() - */ - public static function addCssStyle(&$options, $style, $overwrite = true) - { - if (!empty($options['style'])) { - $oldStyle = static::cssStyleToArray($options['style']); - $newStyle = is_array($style) ? $style : static::cssStyleToArray($style); - if (!$overwrite) { - foreach ($newStyle as $property => $value) { - if (isset($oldStyle[$property])) { - unset($newStyle[$property]); - } - } - } - $style = static::cssStyleFromArray(array_merge($oldStyle, $newStyle)); - } - $options['style'] = $style; - } - - /** - * Removes the specified CSS style from the HTML options. - * - * For example, - * - * ```php - * Html::removeCssStyle($options, ['width', 'height']); - * ``` - * - * @param array $options the HTML options to be modified. - * @param string|array $properties the CSS properties to be removed. You may use a string - * if you are removing a single property. - * @see addCssStyle() - */ - public static function removeCssStyle(&$options, $properties) - { - if (!empty($options['style'])) { - $style = static::cssStyleToArray($options['style']); - foreach ((array)$properties as $property) { - unset($style[$property]); - } - $options['style'] = static::cssStyleFromArray($style); - } - } - - /** - * Converts a CSS style array into a string representation. - * - * For example, - * - * ```php - * print_r(Html::cssStyleFromArray(['width' => '100px', 'height' => '200px'])); - * // will display: 'width: 100px; height: 200px;' - * ``` - * - * @param array $style the CSS style array. The array keys are the CSS property names, - * and the array values are the corresponding CSS property values. - * @return string the CSS style string. If the CSS style is empty, a null will be returned. - */ - public static function cssStyleFromArray(array $style) - { - $result = ''; - foreach ($style as $name => $value) { - $result .= "$name: $value; "; - } - // return null if empty to avoid rendering the "style" attribute - return $result === '' ? null : rtrim($result); - } - - /** - * Converts a CSS style string into an array representation. - * - * The array keys are the CSS property names, and the array values - * are the corresponding CSS property values. - * - * For example, - * - * ```php - * print_r(Html::cssStyleToArray('width: 100px; height: 200px;')); - * // will display: ['width' => '100px', 'height' => '200px'] - * ``` - * - * @param string $style the CSS style string - * @return array the array representation of the CSS style - */ - public static function cssStyleToArray($style) - { - $result = []; - foreach (explode(';', $style) as $property) { - $property = explode(':', $property); - if (count($property) > 1) { - $result[trim($property[0])] = trim($property[1]); - } - } - return $result; - } - - /** - * Returns the real attribute name from the given attribute expression. - * - * An attribute expression is an attribute name prefixed and/or suffixed with array indexes. - * It is mainly used in tabular data input and/or input of array type. Below are some examples: - * - * - `[0]content` is used in tabular data input to represent the "content" attribute - * for the first model in tabular input; - * - `dates[0]` represents the first array element of the "dates" attribute; - * - `[0]dates[0]` represents the first array element of the "dates" attribute - * for the first model in tabular input. - * - * If `$attribute` has neither prefix nor suffix, it will be returned back without change. - * @param string $attribute the attribute name or expression - * @return string the attribute name without prefix and suffix. - * @throws InvalidParamException if the attribute name contains non-word characters. - */ - public static function getAttributeName($attribute) - { - if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { - return $matches[2]; - } else { - throw new InvalidParamException('Attribute name must contain word characters only.'); - } - } - - /** - * Returns the value of the specified attribute name or expression. - * - * For an attribute expression like `[0]dates[0]`, this method will return the value of `$model->dates[0]`. - * See [[getAttributeName()]] for more details about attribute expression. - * - * If an attribute value is an instance of [[ActiveRecordInterface]] or an array of such instances, - * the primary value(s) of the AR instance(s) will be returned instead. - * - * @param Model $model the model object - * @param string $attribute the attribute name or expression - * @return string|array the corresponding attribute value - * @throws InvalidParamException if the attribute name contains non-word characters. - */ - public static function getAttributeValue($model, $attribute) - { - if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { - throw new InvalidParamException('Attribute name must contain word characters only.'); - } - $attribute = $matches[2]; - $value = $model->$attribute; - if ($matches[3] !== '') { - foreach (explode('][', trim($matches[3], '[]')) as $id) { - if ((is_array($value) || $value instanceof \ArrayAccess) && isset($value[$id])) { - $value = $value[$id]; - } else { - return null; - } - } - } - - // https://github.com/yiisoft/yii2/issues/1457 - if (is_array($value)) { - foreach ($value as $i => $v) { - if ($v instanceof ActiveRecordInterface) { - $v = $v->getPrimaryKey(false); - $value[$i] = is_array($v) ? json_encode($v) : $v; - } - } - } elseif ($value instanceof ActiveRecordInterface) { - $value = $value->getPrimaryKey(false); - return is_array($value) ? json_encode($value) : $value; - } - return $value; - } - - /** - * Generates an appropriate input name for the specified attribute name or expression. - * - * This method generates a name that can be used as the input name to collect user input - * for the specified attribute. The name is generated according to the [[Model::formName|form name]] - * of the model and the given attribute name. For example, if the form name of the `Post` model - * is `Post`, then the input name generated for the `content` attribute would be `Post[content]`. - * - * See [[getAttributeName()]] for explanation of attribute expression. - * - * @param Model $model the model object - * @param string $attribute the attribute name or expression - * @return string the generated input name - * @throws InvalidParamException if the attribute name contains non-word characters. - */ - public static function getInputName($model, $attribute) - { - $formName = $model->formName(); - if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { - throw new InvalidParamException('Attribute name must contain word characters only.'); - } - $prefix = $matches[1]; - $attribute = $matches[2]; - $suffix = $matches[3]; - if ($formName === '' && $prefix === '') { - return $attribute . $suffix; - } elseif ($formName !== '') { - return $formName . $prefix . "[$attribute]" . $suffix; - } else { - throw new InvalidParamException(get_class($model) . '::formName() cannot be empty for tabular inputs.'); - } - } - - /** - * Generates an appropriate input ID for the specified attribute name or expression. - * - * This method converts the result [[getInputName()]] into a valid input ID. - * For example, if [[getInputName()]] returns `Post[content]`, this method will return `post-content`. - * @param Model $model the model object - * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for explanation of attribute expression. - * @return string the generated input ID - * @throws InvalidParamException if the attribute name contains non-word characters. - */ - public static function getInputId($model, $attribute) - { - $name = strtolower(static::getInputName($model, $attribute)); - return str_replace(['[]', '][', '[', ']', ' '], ['', '-', '-', '', '-'], $name); - } + /** + * @var array list of void elements (element name => 1) + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = [ + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ]; + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderTagAttributes()]]. + */ + public static $attributeOrder = [ + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ]; + + /** + * Encodes special characters into HTML entities. + * The [[\yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @param boolean $doubleEncode whether to encode HTML entities in `$content`. If false, + * HTML entities in `$content` will not be further encoded. + * @return string the encoded content + * @see decode() + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content, $doubleEncode = true) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset, $doubleEncode); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode() + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the HTML tag attributes (HTML options) in terms of name-value pairs. + * These will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * + * For example when using `['class' => 'my-class', 'target' => '_blank', 'value' => null]` it will result in the + * html attributes rendered like this: `class="my-class" target="_blank"`. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated HTML tag + * @see beginTag() + * @see endTag() + */ + public static function tag($name, $content = '', $options = []) + { + $html = "<$name" . static::renderTagAttributes($options) . '>'; + + return isset(static::$voidElements[strtolower($name)]) ? $html : "$html$content"; + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated start tag + * @see endTag() + * @see tag() + */ + public static function beginTag($name, $options = []) + { + return "<$name" . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag() + * @see tag() + */ + public static function endTag($name) + { + return ""; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated style tag + */ + public static function style($content, $options = []) + { + return static::tag('style', $content, $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated script tag + */ + public static function script($content, $options = []) + { + return static::tag('script', $content, $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[\yii\helpers\Url::to()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated link tag + * @see \yii\helpers\Url::to() + */ + public static function cssFile($url, $options = []) + { + if (!isset($options['rel'])) { + $options['rel'] = 'stylesheet'; + } + $options['href'] = Url::to($url); + + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[\yii\helpers\Url::to()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated script tag + * @see \yii\helpers\Url::to() + */ + public static function jsFile($url, $options = []) + { + $options['src'] = Url::to($url); + + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Url::to()]]. + * @param string $method the form submission method, such as "post", "get", "put", "delete" (case-insensitive). + * Since most browsers only support "post" and "get", if other methods are given, they will + * be simulated using "post", and a hidden input will be added which contains the actual method type. + * See [[\yii\web\Request::methodParam]] for more details. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated form start tag. + * @see endForm() + */ + public static function beginForm($action = '', $method = 'post', $options = []) + { + $action = Url::to($action); + + $hiddenInputs = []; + + $request = Yii::$app->getRequest(); + if ($request instanceof Request) { + if (strcasecmp($method, 'get') && strcasecmp($method, 'post')) { + // simulate PUT, DELETE, etc. via POST + $hiddenInputs[] = static::hiddenInput($request->methodParam, $method); + $method = 'post'; + } + if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { + $hiddenInputs[] = static::hiddenInput($request->csrfParam, $request->getCsrfToken()); + } + } + + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddenInputs[] = static::hiddenInput( + urldecode(substr($pair, 0, $pos1)), + urldecode(substr($pair, $pos1 + 1)) + ); + } else { + $hiddenInputs[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if (!empty($hiddenInputs)) { + $form .= "\n" . implode("\n", $hiddenInputs); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm() + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[yii\helpers\Url::to()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated hyperlink + * @see yii\helpers\Url::to() + */ + public static function a($text, $url = null, $options = []) + { + if ($url !== null) { + $options['href'] = Url::to($url); + } + + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = []) + { + $options['href'] = 'mailto:' . ($email === null ? $text : $email); + + return static::tag('a', $text, $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[yii\helpers\Url::to()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated image tag + */ + public static function img($src, $options = []) + { + $options['src'] = Url::to($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + + return static::tag('img', '', $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = []) + { + $options['for'] = $for; + + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function button($content = 'Button', $options = []) + { + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated submit button tag + */ + public static function submitButton($content = 'Submit', $options = []) + { + $options['type'] = 'submit'; + + return static::button($content, $options); + } + + /** + * Generates a reset button tag. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated reset button tag + */ + public static function resetButton($content = 'Reset', $options = []) + { + $options['type'] = 'reset'; + + return static::button($content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = []) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value === null ? null : (string) $value; + + return static::tag('input', '', $options); + } + + /** + * Generates an input button. + * @param string $label the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function buttonInput($label = 'Button', $options = []) + { + $options['type'] = 'button'; + $options['value'] = $label; + + return static::tag('input', '', $options); + } + + /** + * Generates a submit input button. + * @param string $label the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function submitInput($label = 'Submit', $options = []) + { + $options['type'] = 'submit'; + $options['value'] = $label; + + return static::tag('input', '', $options); + } + + /** + * Generates a reset input button. + * @param string $label the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function resetInput($label = 'Reset', $options = []) + { + $options['type'] = 'reset'; + $options['value'] = $label; + + return static::tag('input', '', $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = []) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = []) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = []) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = []) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = []) + { + $options['name'] = $name; + + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass + * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. + * When this option is specified, the radio button will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. + * - container: array|boolean, the HTML attributes for the container tag. This is only used when the "label" option is specified. + * If it is false, no container will be rendered. If it is an array or not, a "div" container will be rendered + * around the the radio button. + * + * The rest of the options will be rendered as the attributes of the resulting radio button tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $options = []) + { + $options['checked'] = (boolean) $checked; + $value = array_key_exists('value', $options) ? $options['value'] : '1'; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + if (isset($options['label'])) { + $label = $options['label']; + $labelOptions = isset($options['labelOptions']) ? $options['labelOptions'] : []; + $container = isset($options['container']) ? $options['container'] : ['class' => 'radio']; + unset($options['label'], $options['labelOptions'], $options['container']); + $content = static::label(static::input('radio', $name, $value, $options) . ' ' . $label, null, $labelOptions); + if (is_array($container)) { + return $hidden . static::tag('div', $content, $container); + } else { + return $hidden . $content; + } + } else { + return $hidden . static::input('radio', $name, $value, $options); + } + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass + * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. + * When this option is specified, the checkbox will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. + * - container: array|boolean, the HTML attributes for the container tag. This is only used when the "label" option is specified. + * If it is false, no container will be rendered. If it is an array or not, a "div" container will be rendered + * around the the radio button. + * + * The rest of the options will be rendered as the attributes of the resulting checkbox tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $options = []) + { + $options['checked'] = (boolean) $checked; + $value = array_key_exists('value', $options) ? $options['value'] : '1'; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + if (isset($options['label'])) { + $label = $options['label']; + $labelOptions = isset($options['labelOptions']) ? $options['labelOptions'] : []; + $container = isset($options['container']) ? $options['container'] : ['class' => 'checkbox']; + unset($options['label'], $options['labelOptions'], $options['container']); + $content = static::label(static::input('checkbox', $name, $value, $options) . ' ' . $label, null, $labelOptions); + if (is_array($container)) { + return $hidden . static::tag('div', $content, $container); + } else { + return $hidden . $content; + } + } else { + return $hidden . static::input('checkbox', $name, $value, $options); + } + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * [ + * 'value1' => ['disabled' => true], + * 'value2' => ['label' => 'value 2'], + * ]; + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated drop-down list tag + */ + public static function dropDownList($name, $selection = null, $items = [], $options = []) + { + if (!empty($options['multiple'])) { + return static::listBox($name, $selection, $items, $options); + } + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + + return static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param string|array $selection the selected value(s) + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * [ + * 'value1' => ['disabled' => true], + * 'value2' => ['label' => 'value 2'], + * ]; + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated list box tag + */ + public static function listBox($name, $selection = null, $items = [], $options = []) + { + if (!array_key_exists('size', $options)) { + $options['size'] = 4; + } + if (!empty($options['multiple']) && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array values are the labels, while the array keys are the corresponding checkbox values. + * @param array $options options (name => config) for the checkbox list container tag. + * The following options are specially handled: + * + * - tag: string, the tag name of the container element. + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. + * - separator: string, the HTML code that separates items. + * - itemOptions: array, the options for generating the radio button tag using [[checkbox()]]. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input, respectively. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = [], $options = []) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : []; + $encode = !isset($options['encode']) || $options['encode']; + $lines = []; + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::checkbox($name, $checked, array_merge($itemOptions, [ + 'value' => $value, + 'label' => $encode ? static::encode($label) : $label, + ])); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag'], $options['unselect'], $options['encode'], $options['separator'], $options['item'], $options['itemOptions']); + + return $hidden . static::tag($tag, implode($separator, $lines), $options); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array values are the labels, while the array keys are the corresponding radio button values. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. + * - separator: string, the HTML code that separates items. + * - itemOptions: array, the options for generating the radio button tag using [[radio()]]. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input, respectively. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = [], $options = []) + { + $encode = !isset($options['encode']) || $options['encode']; + $formatter = isset($options['item']) ? $options['item'] : null; + $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : []; + $lines = []; + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::radio($name, $checked, array_merge($itemOptions, [ + 'value' => $value, + 'label' => $encode ? static::encode($label) : $label, + ])); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag'], $options['unselect'], $options['encode'], $options['separator'], $options['item'], $options['itemOptions']); + + return $hidden . static::tag($tag, implode($separator, $lines), $options); + } + + /** + * Generates an unordered list. + * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. + * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * This option is ignored if the `item` option is specified. + * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. + * - item: callable, a callback that is used to generate each individual list item. + * The signature of this callback must be: + * + * ~~~ + * function ($item, $index) + * ~~~ + * + * where $index is the array key corresponding to `$item` in `$items`. The callback should return + * the whole list item tag. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated unordered list. An empty string is returned if `$items` is empty. + */ + public static function ul($items, $options = []) + { + if (empty($items)) { + return ''; + } + $tag = isset($options['tag']) ? $options['tag'] : 'ul'; + $encode = !isset($options['encode']) || $options['encode']; + $formatter = isset($options['item']) ? $options['item'] : null; + $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : []; + unset($options['tag'], $options['encode'], $options['item'], $options['itemOptions']); + $results = []; + foreach ($items as $index => $item) { + if ($formatter !== null) { + $results[] = call_user_func($formatter, $item, $index); + } else { + $results[] = static::tag('li', $encode ? static::encode($item) : $item, $itemOptions); + } + } + + return static::tag($tag, "\n" . implode("\n", $results) . "\n", $options); + } + + /** + * Generates an ordered list. + * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. + * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * This option is ignored if the `item` option is specified. + * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. + * - item: callable, a callback that is used to generate each individual list item. + * The signature of this callback must be: + * + * ~~~ + * function ($item, $index) + * ~~~ + * + * where $index is the array key corresponding to `$item` in `$items`. The callback should return + * the whole list item tag. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated ordered list. An empty string is returned if `$items` is empty. + */ + public static function ol($items, $options = []) + { + $options['tag'] = 'ol'; + + return static::ul($items, $options); + } + + /** + * Generates a label tag for the given model attribute. + * The label text is the label associated with the attribute, obtained via [[Model::getAttributeLabel()]]. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * The following options are specially handled: + * + * - label: this specifies the label to be displayed. Note that this will NOT be [[encode()|encoded]]. + * If this is not set, [[Model::getAttributeLabel()]] will be called to get the label for display + * (after encoding). + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated label tag + */ + public static function activeLabel($model, $attribute, $options = []) + { + $for = array_key_exists('for', $options) ? $options['for'] : static::getInputId($model, $attribute); + $attribute = static::getAttributeName($attribute); + $label = isset($options['label']) ? $options['label'] : static::encode($model->getAttributeLabel($attribute)); + unset($options['label'], $options['for']); + + return static::label($label, $for, $options); + } + + /** + * Generates a tag that contains the first validation error of the specified model attribute. + * Note that even if there is no validation error, this method will still return an empty error tag. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. The values will be HTML-encoded + * using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * The following options are specially handled: + * + * - tag: this specifies the tag name. If not set, "div" will be used. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated label tag + */ + public static function error($model, $attribute, $options = []) + { + $attribute = static::getAttributeName($attribute); + $error = $model->getFirstError($attribute); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag']); + + return Html::tag($tag, Html::encode($error), $options); + } + + /** + * Generates an input tag for the given model attribute. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param string $type the input type (e.g. 'text', 'password') + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated input tag + */ + public static function activeInput($type, $model, $attribute, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $value = isset($options['value']) ? $options['value'] : static::getAttributeValue($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::input($type, $name, $value, $options); + } + + /** + * Generates a text input tag for the given model attribute. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated input tag + */ + public static function activeTextInput($model, $attribute, $options = []) + { + return static::activeInput('text', $model, $attribute, $options); + } + + /** + * Generates a hidden input tag for the given model attribute. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated input tag + */ + public static function activeHiddenInput($model, $attribute, $options = []) + { + return static::activeInput('hidden', $model, $attribute, $options); + } + + /** + * Generates a password input tag for the given model attribute. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated input tag + */ + public static function activePasswordInput($model, $attribute, $options = []) + { + return static::activeInput('password', $model, $attribute, $options); + } + + /** + * Generates a file input tag for the given model attribute. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated input tag + */ + public static function activeFileInput($model, $attribute, $options = []) + { + // add a hidden field so that if a model only has a file field, we can + // still use isset($_POST[$modelClass]) to detect if the input is submitted + return static::activeHiddenInput($model, $attribute, ['id' => null, 'value' => '']) + . static::activeInput('file', $model, $attribute, $options); + } + + /** + * Generates a textarea tag for the given model attribute. + * The model attribute value will be used as the content in the textarea. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * @return string the generated textarea tag + */ + public static function activeTextarea($model, $attribute, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $value = static::getAttributeValue($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::textarea($name, $value, $options); + } + + /** + * Generates a radio button tag for the given model attribute. + * This method will generate the "checked" tag attribute according to the model attribute value. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, + * it will take the default value '0'. This method will render a hidden input so that if the radio button + * is not checked and is submitted, the value of this attribute will still be submitted to the server + * via the hidden input. + * - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass + * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. + * When this option is specified, the radio button will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated radio button tag + */ + public static function activeRadio($model, $attribute, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $value = static::getAttributeValue($model, $attribute); + + if (!array_key_exists('value', $options)) { + $options['value'] = '1'; + } + if (!array_key_exists('uncheck', $options)) { + $options['uncheck'] = '0'; + } + + $checked = "$value" === "{$options['value']}"; + + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::radio($name, $checked, $options); + } + + /** + * Generates a checkbox tag for the given model attribute. + * This method will generate the "checked" tag attribute according to the model attribute value. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, + * it will take the default value '0'. This method will render a hidden input so that if the radio button + * is not checked and is submitted, the value of this attribute will still be submitted to the server + * via the hidden input. + * - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass + * in HTML code such as an image tag. If this is is coming from end users, you should [[encode()]] it to prevent XSS attacks. + * When this option is specified, the checkbox will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated checkbox tag + */ + public static function activeCheckbox($model, $attribute, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $value = static::getAttributeValue($model, $attribute); + + if (!array_key_exists('value', $options)) { + $options['value'] = '1'; + } + if (!array_key_exists('uncheck', $options)) { + $options['uncheck'] = '0'; + } + + $checked = "$value" === "{$options['value']}"; + + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::checkbox($name, $checked, $options); + } + + /** + * Generates a drop-down list for the given model attribute. + * The selection of the drop-down list is taken from the value of the model attribute. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * [ + * 'value1' => ['disabled' => true], + * 'value2' => ['label' => 'value 2'], + * ]; + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated drop-down list tag + */ + public static function activeDropDownList($model, $attribute, $items, $options = []) + { + if (!empty($options['multiple'])) { + return static::activeListBox($model, $attribute, $items, $options); + } + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $selection = static::getAttributeValue($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::dropDownList($name, $selection, $items, $options); + } + + /** + * Generates a list box. + * The selection of the list box is taken from the value of the model attribute. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * [ + * 'value1' => ['disabled' => true], + * 'value2' => ['label' => 'value 2'], + * ]; + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated list box tag + */ + public static function activeListBox($model, $attribute, $items, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $selection = static::getAttributeValue($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = ''; + } + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::listBox($name, $selection, $items, $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * The selection of the checkbox list is taken from the value of the model attribute. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $items the data item used to generate the checkboxes. + * The array values are the labels, while the array keys are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are specially handled: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * You may set this option to be null to prevent default value submission. + * If this option is not set, an empty string will be submitted. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated checkbox list + */ + public static function activeCheckboxList($model, $attribute, $items, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $selection = static::getAttributeValue($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = ''; + } + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::checkboxList($name, $selection, $items, $options); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * The selection of the radio buttons is taken from the value of the model attribute. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format + * about attribute expression. + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are specially handled: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * You may set this option to be null to prevent default value submission. + * If this option is not set, an empty string will be submitted. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * + * See [[renderTagAttributes()]] for details on how attributes are being rendered. + * + * @return string the generated radio button list + */ + public static function activeRadioList($model, $attribute, $items, $options = []) + { + $name = isset($options['name']) ? $options['name'] : static::getInputName($model, $attribute); + $selection = static::getAttributeValue($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = ''; + } + if (!array_key_exists('id', $options)) { + $options['id'] = static::getInputId($model, $attribute); + } + + return static::radioList($name, $selection, $items, $options); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = []) + { + $lines = []; + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, ['value' => '']); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : []; + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : []; + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : []; + $groupAttrs['label'] = $key; + $attrs = ['options' => $options, 'groups' => $groups]; + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : []; + $attrs['value'] = (string) $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * + * Attributes whose values are of boolean type will be treated as + * [boolean attributes](http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes). + * + * Attributes whose values are null will not be rendered. + * + * The values of attributes will be HTML-encoded using [[encode()]]. + * + * The "data" attribute is specially handled when it is receiving an array value. In this case, + * the array will be "expanded" and a list data attributes will be rendered. For example, + * if `'data' => ['id' => 1, 'name' => 'yii']`, then this will be rendered: + * `data-id="1" data-name="yii"`. + * + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (so that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = []; + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (is_bool($value)) { + if ($value) { + $html .= " $name"; + } + } elseif (is_array($value) && $name === 'data') { + foreach ($value as $n => $v) { + $html .= " $name-$n=\"" . static::encode($v) . '"'; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + + return $html; + } + + /** + * Adds a CSS class to the specified options. + * If the CSS class is already in the options, it will not be added again. + * @param array $options the options to be modified. + * @param string $class the CSS class to be added + */ + public static function addCssClass(&$options, $class) + { + if (isset($options['class'])) { + $classes = ' ' . $options['class'] . ' '; + if (strpos($classes, ' ' . $class . ' ') === false) { + $options['class'] .= ' ' . $class; + } + } else { + $options['class'] = $class; + } + } + + /** + * Removes a CSS class from the specified options. + * @param array $options the options to be modified. + * @param string $class the CSS class to be removed + */ + public static function removeCssClass(&$options, $class) + { + if (isset($options['class'])) { + $classes = array_unique(preg_split('/\s+/', $options['class'] . ' ' . $class, -1, PREG_SPLIT_NO_EMPTY)); + if (($index = array_search($class, $classes)) !== false) { + unset($classes[$index]); + } + if (empty($classes)) { + unset($options['class']); + } else { + $options['class'] = implode(' ', $classes); + } + } + } + + /** + * Adds the specified CSS style to the HTML options. + * + * If the options already contain a `style` element, the new style will be merged + * with the existing one. If a CSS property exists in both the new and the old styles, + * the old one may be overwritten if `$overwrite` is true. + * + * For example, + * + * ```php + * Html::addCssStyle($options, 'width: 100px; height: 200px'); + * ``` + * + * @param array $options the HTML options to be modified. + * @param string|array $style the new style string (e.g. `'width: 100px; height: 200px'`) or + * array (e.g. `['width' => '100px', 'height' => '200px']`). + * @param boolean $overwrite whether to overwrite existing CSS properties if the new style + * contain them too. + * @see removeCssStyle() + * @see cssStyleFromArray() + * @see cssStyleToArray() + */ + public static function addCssStyle(&$options, $style, $overwrite = true) + { + if (!empty($options['style'])) { + $oldStyle = static::cssStyleToArray($options['style']); + $newStyle = is_array($style) ? $style : static::cssStyleToArray($style); + if (!$overwrite) { + foreach ($newStyle as $property => $value) { + if (isset($oldStyle[$property])) { + unset($newStyle[$property]); + } + } + } + $style = static::cssStyleFromArray(array_merge($oldStyle, $newStyle)); + } + $options['style'] = $style; + } + + /** + * Removes the specified CSS style from the HTML options. + * + * For example, + * + * ```php + * Html::removeCssStyle($options, ['width', 'height']); + * ``` + * + * @param array $options the HTML options to be modified. + * @param string|array $properties the CSS properties to be removed. You may use a string + * if you are removing a single property. + * @see addCssStyle() + */ + public static function removeCssStyle(&$options, $properties) + { + if (!empty($options['style'])) { + $style = static::cssStyleToArray($options['style']); + foreach ((array) $properties as $property) { + unset($style[$property]); + } + $options['style'] = static::cssStyleFromArray($style); + } + } + + /** + * Converts a CSS style array into a string representation. + * + * For example, + * + * ```php + * print_r(Html::cssStyleFromArray(['width' => '100px', 'height' => '200px'])); + * // will display: 'width: 100px; height: 200px;' + * ``` + * + * @param array $style the CSS style array. The array keys are the CSS property names, + * and the array values are the corresponding CSS property values. + * @return string the CSS style string. If the CSS style is empty, a null will be returned. + */ + public static function cssStyleFromArray(array $style) + { + $result = ''; + foreach ($style as $name => $value) { + $result .= "$name: $value; "; + } + // return null if empty to avoid rendering the "style" attribute + return $result === '' ? null : rtrim($result); + } + + /** + * Converts a CSS style string into an array representation. + * + * The array keys are the CSS property names, and the array values + * are the corresponding CSS property values. + * + * For example, + * + * ```php + * print_r(Html::cssStyleToArray('width: 100px; height: 200px;')); + * // will display: ['width' => '100px', 'height' => '200px'] + * ``` + * + * @param string $style the CSS style string + * @return array the array representation of the CSS style + */ + public static function cssStyleToArray($style) + { + $result = []; + foreach (explode(';', $style) as $property) { + $property = explode(':', $property); + if (count($property) > 1) { + $result[trim($property[0])] = trim($property[1]); + } + } + + return $result; + } + + /** + * Returns the real attribute name from the given attribute expression. + * + * An attribute expression is an attribute name prefixed and/or suffixed with array indexes. + * It is mainly used in tabular data input and/or input of array type. Below are some examples: + * + * - `[0]content` is used in tabular data input to represent the "content" attribute + * for the first model in tabular input; + * - `dates[0]` represents the first array element of the "dates" attribute; + * - `[0]dates[0]` represents the first array element of the "dates" attribute + * for the first model in tabular input. + * + * If `$attribute` has neither prefix nor suffix, it will be returned back without change. + * @param string $attribute the attribute name or expression + * @return string the attribute name without prefix and suffix. + * @throws InvalidParamException if the attribute name contains non-word characters. + */ + public static function getAttributeName($attribute) + { + if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + return $matches[2]; + } else { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + } + + /** + * Returns the value of the specified attribute name or expression. + * + * For an attribute expression like `[0]dates[0]`, this method will return the value of `$model->dates[0]`. + * See [[getAttributeName()]] for more details about attribute expression. + * + * If an attribute value is an instance of [[ActiveRecordInterface]] or an array of such instances, + * the primary value(s) of the AR instance(s) will be returned instead. + * + * @param Model $model the model object + * @param string $attribute the attribute name or expression + * @return string|array the corresponding attribute value + * @throws InvalidParamException if the attribute name contains non-word characters. + */ + public static function getAttributeValue($model, $attribute) + { + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $attribute = $matches[2]; + $value = $model->$attribute; + if ($matches[3] !== '') { + foreach (explode('][', trim($matches[3], '[]')) as $id) { + if ((is_array($value) || $value instanceof \ArrayAccess) && isset($value[$id])) { + $value = $value[$id]; + } else { + return null; + } + } + } + + // https://github.com/yiisoft/yii2/issues/1457 + if (is_array($value)) { + foreach ($value as $i => $v) { + if ($v instanceof ActiveRecordInterface) { + $v = $v->getPrimaryKey(false); + $value[$i] = is_array($v) ? json_encode($v) : $v; + } + } + } elseif ($value instanceof ActiveRecordInterface) { + $value = $value->getPrimaryKey(false); + + return is_array($value) ? json_encode($value) : $value; + } + + return $value; + } + + /** + * Generates an appropriate input name for the specified attribute name or expression. + * + * This method generates a name that can be used as the input name to collect user input + * for the specified attribute. The name is generated according to the [[Model::formName|form name]] + * of the model and the given attribute name. For example, if the form name of the `Post` model + * is `Post`, then the input name generated for the `content` attribute would be `Post[content]`. + * + * See [[getAttributeName()]] for explanation of attribute expression. + * + * @param Model $model the model object + * @param string $attribute the attribute name or expression + * @return string the generated input name + * @throws InvalidParamException if the attribute name contains non-word characters. + */ + public static function getInputName($model, $attribute) + { + $formName = $model->formName(); + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $prefix = $matches[1]; + $attribute = $matches[2]; + $suffix = $matches[3]; + if ($formName === '' && $prefix === '') { + return $attribute . $suffix; + } elseif ($formName !== '') { + return $formName . $prefix . "[$attribute]" . $suffix; + } else { + throw new InvalidParamException(get_class($model) . '::formName() cannot be empty for tabular inputs.'); + } + } + + /** + * Generates an appropriate input ID for the specified attribute name or expression. + * + * This method converts the result [[getInputName()]] into a valid input ID. + * For example, if [[getInputName()]] returns `Post[content]`, this method will return `post-content`. + * @param Model $model the model object + * @param string $attribute the attribute name or expression. See [[getAttributeName()]] for explanation of attribute expression. + * @return string the generated input ID + * @throws InvalidParamException if the attribute name contains non-word characters. + */ + public static function getInputId($model, $attribute) + { + $name = strtolower(static::getInputName($model, $attribute)); + + return str_replace(['[]', '][', '[', ']', ' '], ['', '-', '-', '', '-'], $name); + } } diff --git a/framework/helpers/BaseHtmlPurifier.php b/framework/helpers/BaseHtmlPurifier.php index 17d21226bcb..7f3d8569d1f 100644 --- a/framework/helpers/BaseHtmlPurifier.php +++ b/framework/helpers/BaseHtmlPurifier.php @@ -16,19 +16,20 @@ */ class BaseHtmlPurifier { - /** - * Passes markup through HTMLPurifier making it safe to output to end user - * - * @param string $content - * @param array|null $config - * @return string - */ - public static function process($content, $config = null) - { - $configInstance = \HTMLPurifier_Config::create($config); - $configInstance->autoFinalize = false; - $purifier=\HTMLPurifier::instance($configInstance); - $purifier->config->set('Cache.SerializerPath', \Yii::$app->getRuntimePath()); - return $purifier->purify($content); - } + /** + * Passes markup through HTMLPurifier making it safe to output to end user + * + * @param string $content + * @param array|null $config + * @return string + */ + public static function process($content, $config = null) + { + $configInstance = \HTMLPurifier_Config::create($config); + $configInstance->autoFinalize = false; + $purifier=\HTMLPurifier::instance($configInstance); + $purifier->config->set('Cache.SerializerPath', \Yii::$app->getRuntimePath()); + + return $purifier->purify($content); + } } diff --git a/framework/helpers/BaseInflector.php b/framework/helpers/BaseInflector.php index 1988945c20d..2ca4bd079de 100644 --- a/framework/helpers/BaseInflector.php +++ b/framework/helpers/BaseInflector.php @@ -19,484 +19,491 @@ */ class BaseInflector { - /** - * @var array the rules for converting a word into its plural form. - * The keys are the regular expressions and the values are the corresponding replacements. - */ - public static $plurals = [ - '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media)$/i' => '\1', - '/^(sea[- ]bass)$/i' => '\1', - '/(m)ove$/i' => '\1oves', - '/(f)oot$/i' => '\1eet', - '/(h)uman$/i' => '\1umans', - '/(s)tatus$/i' => '\1tatuses', - '/(s)taff$/i' => '\1taff', - '/(t)ooth$/i' => '\1eeth', - '/(quiz)$/i' => '\1zes', - '/^(ox)$/i' => '\1\2en', - '/([m|l])ouse$/i' => '\1ice', - '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', - '/(x|ch|ss|sh)$/i' => '\1es', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(hive)$/i' => '\1s', - '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', - '/sis$/i' => 'ses', - '/([ti])um$/i' => '\1a', - '/(p)erson$/i' => '\1eople', - '/(m)an$/i' => '\1en', - '/(c)hild$/i' => '\1hildren', - '/(buffal|tomat|potat|ech|her|vet)o$/i' => '\1oes', - '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i', - '/us$/i' => 'uses', - '/(alias)$/i' => '\1es', - '/(ax|cris|test)is$/i' => '\1es', - '/s$/' => 's', - '/^$/' => '', - '/$/' => 's', - ]; - /** - * @var array the rules for converting a word into its singular form. - * The keys are the regular expressions and the values are the corresponding replacements. - */ - public static $singulars = [ - '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media|ss)$/i' => '\1', - '/^(sea[- ]bass)$/i' => '\1', - '/(s)tatuses$/i' => '\1tatus', - '/(f)eet$/i' => '\1oot', - '/(t)eeth$/i' => '\1ooth', - '/^(.*)(menu)s$/i' => '\1\2', - '/(quiz)zes$/i' => '\\1', - '/(matr)ices$/i' => '\1ix', - '/(vert|ind)ices$/i' => '\1ex', - '/^(ox)en/i' => '\1', - '/(alias)(es)*$/i' => '\1', - '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', - '/([ftw]ax)es/i' => '\1', - '/(cris|ax|test)es$/i' => '\1is', - '/(shoe|slave)s$/i' => '\1', - '/(o)es$/i' => '\1', - '/ouses$/' => 'ouse', - '/([^a])uses$/' => '\1us', - '/([m|l])ice$/i' => '\1ouse', - '/(x|ch|ss|sh)es$/i' => '\1', - '/(m)ovies$/i' => '\1\2ovie', - '/(s)eries$/i' => '\1\2eries', - '/([^aeiouy]|qu)ies$/i' => '\1y', - '/([lr])ves$/i' => '\1f', - '/(tive)s$/i' => '\1', - '/(hive)s$/i' => '\1', - '/(drive)s$/i' => '\1', - '/([^fo])ves$/i' => '\1fe', - '/(^analy)ses$/i' => '\1sis', - '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', - '/([ti])a$/i' => '\1um', - '/(p)eople$/i' => '\1\2erson', - '/(m)en$/i' => '\1an', - '/(c)hildren$/i' => '\1\2hild', - '/(n)ews$/i' => '\1\2ews', - '/eaus$/' => 'eau', - '/^(.*us)$/' => '\\1', - '/s$/i' => '', - ]; - /** - * @var array the special rules for converting a word between its plural form and singular form. - * The keys are the special words in singular form, and the values are the corresponding plural form. - */ - public static $specials = [ - 'atlas' => 'atlases', - 'beef' => 'beefs', - 'brother' => 'brothers', - 'cafe' => 'cafes', - 'child' => 'children', - 'cookie' => 'cookies', - 'corpus' => 'corpuses', - 'cow' => 'cows', - 'curve' => 'curves', - 'foe' => 'foes', - 'ganglion' => 'ganglions', - 'genie' => 'genies', - 'genus' => 'genera', - 'graffito' => 'graffiti', - 'hoof' => 'hoofs', - 'loaf' => 'loaves', - 'man' => 'men', - 'money' => 'monies', - 'mongoose' => 'mongooses', - 'move' => 'moves', - 'mythos' => 'mythoi', - 'niche' => 'niches', - 'numen' => 'numina', - 'occiput' => 'occiputs', - 'octopus' => 'octopuses', - 'opus' => 'opuses', - 'ox' => 'oxen', - 'penis' => 'penises', - 'sex' => 'sexes', - 'soliloquy' => 'soliloquies', - 'testis' => 'testes', - 'trilby' => 'trilbys', - 'turf' => 'turfs', - 'wave' => 'waves', - 'Amoyese' => 'Amoyese', - 'bison' => 'bison', - 'Borghese' => 'Borghese', - 'bream' => 'bream', - 'breeches' => 'breeches', - 'britches' => 'britches', - 'buffalo' => 'buffalo', - 'cantus' => 'cantus', - 'carp' => 'carp', - 'chassis' => 'chassis', - 'clippers' => 'clippers', - 'cod' => 'cod', - 'coitus' => 'coitus', - 'Congoese' => 'Congoese', - 'contretemps' => 'contretemps', - 'corps' => 'corps', - 'debris' => 'debris', - 'diabetes' => 'diabetes', - 'djinn' => 'djinn', - 'eland' => 'eland', - 'elk' => 'elk', - 'equipment' => 'equipment', - 'Faroese' => 'Faroese', - 'flounder' => 'flounder', - 'Foochowese' => 'Foochowese', - 'gallows' => 'gallows', - 'Genevese' => 'Genevese', - 'Genoese' => 'Genoese', - 'Gilbertese' => 'Gilbertese', - 'graffiti' => 'graffiti', - 'headquarters' => 'headquarters', - 'herpes' => 'herpes', - 'hijinks' => 'hijinks', - 'Hottentotese' => 'Hottentotese', - 'information' => 'information', - 'innings' => 'innings', - 'jackanapes' => 'jackanapes', - 'Kiplingese' => 'Kiplingese', - 'Kongoese' => 'Kongoese', - 'Lucchese' => 'Lucchese', - 'mackerel' => 'mackerel', - 'Maltese' => 'Maltese', - 'mews' => 'mews', - 'moose' => 'moose', - 'mumps' => 'mumps', - 'Nankingese' => 'Nankingese', - 'news' => 'news', - 'nexus' => 'nexus', - 'Niasese' => 'Niasese', - 'Pekingese' => 'Pekingese', - 'Piedmontese' => 'Piedmontese', - 'pincers' => 'pincers', - 'Pistoiese' => 'Pistoiese', - 'pliers' => 'pliers', - 'Portuguese' => 'Portuguese', - 'proceedings' => 'proceedings', - 'rabies' => 'rabies', - 'rice' => 'rice', - 'rhinoceros' => 'rhinoceros', - 'salmon' => 'salmon', - 'Sarawakese' => 'Sarawakese', - 'scissors' => 'scissors', - 'series' => 'series', - 'Shavese' => 'Shavese', - 'shears' => 'shears', - 'siemens' => 'siemens', - 'species' => 'species', - 'swine' => 'swine', - 'testes' => 'testes', - 'trousers' => 'trousers', - 'trout' => 'trout', - 'tuna' => 'tuna', - 'Vermontese' => 'Vermontese', - 'Wenchowese' => 'Wenchowese', - 'whiting' => 'whiting', - 'wildebeest' => 'wildebeest', - 'Yengeese' => 'Yengeese', - ]; + /** + * @var array the rules for converting a word into its plural form. + * The keys are the regular expressions and the values are the corresponding replacements. + */ + public static $plurals = [ + '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media)$/i' => '\1', + '/^(sea[- ]bass)$/i' => '\1', + '/(m)ove$/i' => '\1oves', + '/(f)oot$/i' => '\1eet', + '/(h)uman$/i' => '\1umans', + '/(s)tatus$/i' => '\1tatuses', + '/(s)taff$/i' => '\1taff', + '/(t)ooth$/i' => '\1eeth', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(m)an$/i' => '\1en', + '/(c)hild$/i' => '\1hildren', + '/(buffal|tomat|potat|ech|her|vet)o$/i' => '\1oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(ax|cris|test)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ]; + /** + * @var array the rules for converting a word into its singular form. + * The keys are the regular expressions and the values are the corresponding replacements. + */ + public static $singulars = [ + '/([nrlm]ese|deer|fish|sheep|measles|ois|pox|media|ss)$/i' => '\1', + '/^(sea[- ]bass)$/i' => '\1', + '/(s)tatuses$/i' => '\1tatus', + '/(f)eet$/i' => '\1oot', + '/(t)eeth$/i' => '\1ooth', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)(es)*$/i' => '\1', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe|slave)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([lr])ves$/i' => '\1f', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([^fo])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '', + ]; + /** + * @var array the special rules for converting a word between its plural form and singular form. + * The keys are the special words in singular form, and the values are the corresponding plural form. + */ + public static $specials = [ + 'atlas' => 'atlases', + 'beef' => 'beefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'curve' => 'curves', + 'foe' => 'foes', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hoof' => 'hoofs', + 'loaf' => 'loaves', + 'man' => 'men', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'testis' => 'testes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'wave' => 'waves', + 'Amoyese' => 'Amoyese', + 'bison' => 'bison', + 'Borghese' => 'Borghese', + 'bream' => 'bream', + 'breeches' => 'breeches', + 'britches' => 'britches', + 'buffalo' => 'buffalo', + 'cantus' => 'cantus', + 'carp' => 'carp', + 'chassis' => 'chassis', + 'clippers' => 'clippers', + 'cod' => 'cod', + 'coitus' => 'coitus', + 'Congoese' => 'Congoese', + 'contretemps' => 'contretemps', + 'corps' => 'corps', + 'debris' => 'debris', + 'diabetes' => 'diabetes', + 'djinn' => 'djinn', + 'eland' => 'eland', + 'elk' => 'elk', + 'equipment' => 'equipment', + 'Faroese' => 'Faroese', + 'flounder' => 'flounder', + 'Foochowese' => 'Foochowese', + 'gallows' => 'gallows', + 'Genevese' => 'Genevese', + 'Genoese' => 'Genoese', + 'Gilbertese' => 'Gilbertese', + 'graffiti' => 'graffiti', + 'headquarters' => 'headquarters', + 'herpes' => 'herpes', + 'hijinks' => 'hijinks', + 'Hottentotese' => 'Hottentotese', + 'information' => 'information', + 'innings' => 'innings', + 'jackanapes' => 'jackanapes', + 'Kiplingese' => 'Kiplingese', + 'Kongoese' => 'Kongoese', + 'Lucchese' => 'Lucchese', + 'mackerel' => 'mackerel', + 'Maltese' => 'Maltese', + 'mews' => 'mews', + 'moose' => 'moose', + 'mumps' => 'mumps', + 'Nankingese' => 'Nankingese', + 'news' => 'news', + 'nexus' => 'nexus', + 'Niasese' => 'Niasese', + 'Pekingese' => 'Pekingese', + 'Piedmontese' => 'Piedmontese', + 'pincers' => 'pincers', + 'Pistoiese' => 'Pistoiese', + 'pliers' => 'pliers', + 'Portuguese' => 'Portuguese', + 'proceedings' => 'proceedings', + 'rabies' => 'rabies', + 'rice' => 'rice', + 'rhinoceros' => 'rhinoceros', + 'salmon' => 'salmon', + 'Sarawakese' => 'Sarawakese', + 'scissors' => 'scissors', + 'series' => 'series', + 'Shavese' => 'Shavese', + 'shears' => 'shears', + 'siemens' => 'siemens', + 'species' => 'species', + 'swine' => 'swine', + 'testes' => 'testes', + 'trousers' => 'trousers', + 'trout' => 'trout', + 'tuna' => 'tuna', + 'Vermontese' => 'Vermontese', + 'Wenchowese' => 'Wenchowese', + 'whiting' => 'whiting', + 'wildebeest' => 'wildebeest', + 'Yengeese' => 'Yengeese', + ]; - /** - * @var array map of special chars and its translation. This is used by [[slug()]]. - */ - public static $transliteration = [ - // Latin - 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C', - 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', - 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O', - 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH', - 'ß' => 'ss', - 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', - 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', - 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o', - 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th', - 'ÿ' => 'y', - // Latin symbols - '©' => '(c)', - // Greek - 'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8', - 'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P', - 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W', - 'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I', - 'Ϋ' => 'Y', - 'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8', - 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p', - 'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w', - 'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's', - 'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i', - // Turkish - 'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G', - 'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g', - // Russian - 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh', - 'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', - 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C', - 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu', - 'Я' => 'Ya', - 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', - 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', - 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', - 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', - 'я' => 'ya', - // Ukrainian - 'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G', - 'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g', - // Czech - 'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U', - 'Ž' => 'Z', - 'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u', - 'ž' => 'z', - // Polish - 'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z', - 'Ż' => 'Z', - 'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z', - 'ż' => 'z', - // Latvian - 'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N', - 'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z', - 'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', - 'š' => 's', 'ū' => 'u', 'ž' => 'z', - //Vietnamese - 'Ấ' => 'A', 'Ầ' => 'A', 'Ẩ' => 'A', 'Ẫ' => 'A', 'Ậ' => 'A', - 'Ắ' => 'A', 'Ằ' => 'A', 'Ẳ' => 'A', 'Ẵ' => 'A', 'Ặ' => 'A', - 'Ố' => 'O', 'Ồ' => 'O', 'Ổ' => 'O', 'Ỗ' => 'O', 'Ộ' => 'O', - 'Ớ' => 'O', 'Ờ' => 'O', 'Ở' => 'O', 'Ỡ' => 'O', 'Ợ' => 'O', - 'Ế' => 'E', 'Ề' => 'E', 'Ể' => 'E', 'Ễ' => 'E', 'Ệ' => 'E', - 'ấ' => 'a', 'ầ' => 'a', 'ẩ' => 'a', 'ẫ' => 'a', 'ậ' => 'a', - 'ắ' => 'a', 'ằ' => 'a', 'ẳ' => 'a', 'ẵ' => 'a', 'ặ' => 'a', - 'ố' => 'o', 'ồ' => 'o', 'ổ' => 'o', 'ỗ' => 'o', 'ộ' => 'o', - 'ớ' => 'o', 'ờ' => 'o', 'ở' => 'o', 'ỡ' => 'o', 'ợ' => 'o', - 'ế' => 'e', 'ề' => 'e', 'ể' => 'e', 'ễ' => 'e', 'ệ' => 'e' - ]; + /** + * @var array map of special chars and its translation. This is used by [[slug()]]. + */ + public static $transliteration = [ + // Latin + 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C', + 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', + 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O', + 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH', + 'ß' => 'ss', + 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', + 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', + 'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o', + 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th', + 'ÿ' => 'y', + // Latin symbols + '©' => '(c)', + // Greek + 'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8', + 'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P', + 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W', + 'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I', + 'Ϋ' => 'Y', + 'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8', + 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p', + 'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w', + 'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's', + 'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i', + // Turkish + 'Ş' => 'S', 'İ' => 'I', 'Ç' => 'C', 'Ü' => 'U', 'Ö' => 'O', 'Ğ' => 'G', + 'ş' => 's', 'ı' => 'i', 'ç' => 'c', 'ü' => 'u', 'ö' => 'o', 'ğ' => 'g', + // Russian + 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh', + 'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', + 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C', + 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu', + 'Я' => 'Ya', + 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', + 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', + 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', + 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', + 'я' => 'ya', + // Ukrainian + 'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G', + 'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g', + // Czech + 'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U', + 'Ž' => 'Z', + 'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u', + 'ž' => 'z', + // Polish + 'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'o', 'Ś' => 'S', 'Ź' => 'Z', + 'Ż' => 'Z', + 'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z', + 'ż' => 'z', + // Latvian + 'Ā' => 'A', 'Č' => 'C', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N', + 'Š' => 'S', 'Ū' => 'u', 'Ž' => 'Z', + 'ā' => 'a', 'č' => 'c', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', + 'š' => 's', 'ū' => 'u', 'ž' => 'z', + //Vietnamese + 'Ấ' => 'A', 'Ầ' => 'A', 'Ẩ' => 'A', 'Ẫ' => 'A', 'Ậ' => 'A', + 'Ắ' => 'A', 'Ằ' => 'A', 'Ẳ' => 'A', 'Ẵ' => 'A', 'Ặ' => 'A', + 'Ố' => 'O', 'Ồ' => 'O', 'Ổ' => 'O', 'Ỗ' => 'O', 'Ộ' => 'O', + 'Ớ' => 'O', 'Ờ' => 'O', 'Ở' => 'O', 'Ỡ' => 'O', 'Ợ' => 'O', + 'Ế' => 'E', 'Ề' => 'E', 'Ể' => 'E', 'Ễ' => 'E', 'Ệ' => 'E', + 'ấ' => 'a', 'ầ' => 'a', 'ẩ' => 'a', 'ẫ' => 'a', 'ậ' => 'a', + 'ắ' => 'a', 'ằ' => 'a', 'ẳ' => 'a', 'ẵ' => 'a', 'ặ' => 'a', + 'ố' => 'o', 'ồ' => 'o', 'ổ' => 'o', 'ỗ' => 'o', 'ộ' => 'o', + 'ớ' => 'o', 'ờ' => 'o', 'ở' => 'o', 'ỡ' => 'o', 'ợ' => 'o', + 'ế' => 'e', 'ề' => 'e', 'ể' => 'e', 'ễ' => 'e', 'ệ' => 'e' + ]; - /** - * Converts a word to its plural form. - * Note that this is for English only! - * For example, 'apple' will become 'apples', and 'child' will become 'children'. - * @param string $word the word to be pluralized - * @return string the pluralized word - */ - public static function pluralize($word) - { - if (isset(static::$specials[$word])) { - return static::$specials[$word]; - } - foreach (static::$plurals as $rule => $replacement) { - if (preg_match($rule, $word)) { - return preg_replace($rule, $replacement, $word); - } - } - return $word; - } + /** + * Converts a word to its plural form. + * Note that this is for English only! + * For example, 'apple' will become 'apples', and 'child' will become 'children'. + * @param string $word the word to be pluralized + * @return string the pluralized word + */ + public static function pluralize($word) + { + if (isset(static::$specials[$word])) { + return static::$specials[$word]; + } + foreach (static::$plurals as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } - /** - * Returns the singular of the $word - * @param string $word the english word to singularize - * @return string Singular noun. - */ - public static function singularize($word) - { - $result = array_search($word, static::$specials, true); - if ($result !== false) { - return $result; - } - foreach (static::$singulars as $rule => $replacement) { - if (preg_match($rule, $word)) { - return preg_replace($rule, $replacement, $word); - } - } - return $word; - } + return $word; + } - /** - * Converts an underscored or CamelCase word into a English - * sentence. - * @param string $words - * @param boolean $ucAll whether to set all words to uppercase - * @return string - */ - public static function titleize($words, $ucAll = false) - { - $words = static::humanize(static::underscore($words), $ucAll); - return $ucAll ? ucwords($words) : ucfirst($words); - } + /** + * Returns the singular of the $word + * @param string $word the english word to singularize + * @return string Singular noun. + */ + public static function singularize($word) + { + $result = array_search($word, static::$specials, true); + if ($result !== false) { + return $result; + } + foreach (static::$singulars as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } - /** - * Returns given word as CamelCased - * Converts a word like "send_email" to "SendEmail". It - * will remove non alphanumeric character from the word, so - * "who's online" will be converted to "WhoSOnline" - * @see variablize() - * @param string $word the word to CamelCase - * @return string - */ - public static function camelize($word) - { - return str_replace(' ', '', ucwords(preg_replace('/[^A-Za-z0-9]+/', ' ', $word))); - } + return $word; + } - /** - * Converts a CamelCase name into space-separated words. - * For example, 'PostTag' will be converted to 'Post Tag'. - * @param string $name the string to be converted - * @param boolean $ucwords whether to capitalize the first letter in each word - * @return string the resulting words - */ - public static function camel2words($name, $ucwords = true) - { - $label = trim(strtolower(str_replace([ - '-', - '_', - '.' - ], ' ', preg_replace('/(? - * @return string the encoding result - */ - public static function encode($value, $options = 0) - { - $expressions = []; - $value = static::processData($value, $expressions, uniqid()); - $json = json_encode($value, $options); - return empty($expressions) ? $json : strtr($json, $expressions); - } + /** + * Encodes the given value into a JSON string. + * The method enhances `json_encode()` by supporting JavaScript expressions. + * In particular, the method will not encode a JavaScript expression that is + * represented in terms of a [[JsExpression]] object. + * @param mixed $value the data to be encoded + * @param integer $options the encoding options. For more details please refer to + * + * @return string the encoding result + */ + public static function encode($value, $options = 0) + { + $expressions = []; + $value = static::processData($value, $expressions, uniqid()); + $json = json_encode($value, $options); - /** - * Decodes the given JSON string into a PHP data structure. - * @param string $json the JSON string to be decoded - * @param boolean $asArray whether to return objects in terms of associative arrays. - * @return mixed the PHP data - * @throws InvalidParamException if there is any decoding error - */ - public static function decode($json, $asArray = true) - { - if (is_array($json)) { - throw new InvalidParamException('Invalid JSON data.'); - } - $decode = json_decode((string)$json, $asArray); - switch (json_last_error()) { - case JSON_ERROR_NONE: - break; - case JSON_ERROR_DEPTH: - throw new InvalidParamException('The maximum stack depth has been exceeded.'); - case JSON_ERROR_CTRL_CHAR: - throw new InvalidParamException('Control character error, possibly incorrectly encoded.'); - case JSON_ERROR_SYNTAX: - throw new InvalidParamException('Syntax error.'); - case JSON_ERROR_STATE_MISMATCH: - throw new InvalidParamException('Invalid or malformed JSON.'); - case JSON_ERROR_UTF8: - throw new InvalidParamException('Malformed UTF-8 characters, possibly incorrectly encoded.'); - default: - throw new InvalidParamException('Unknown JSON decoding error.'); - } + return empty($expressions) ? $json : strtr($json, $expressions); + } - return $decode; - } + /** + * Decodes the given JSON string into a PHP data structure. + * @param string $json the JSON string to be decoded + * @param boolean $asArray whether to return objects in terms of associative arrays. + * @return mixed the PHP data + * @throws InvalidParamException if there is any decoding error + */ + public static function decode($json, $asArray = true) + { + if (is_array($json)) { + throw new InvalidParamException('Invalid JSON data.'); + } + $decode = json_decode((string) $json, $asArray); + switch (json_last_error()) { + case JSON_ERROR_NONE: + break; + case JSON_ERROR_DEPTH: + throw new InvalidParamException('The maximum stack depth has been exceeded.'); + case JSON_ERROR_CTRL_CHAR: + throw new InvalidParamException('Control character error, possibly incorrectly encoded.'); + case JSON_ERROR_SYNTAX: + throw new InvalidParamException('Syntax error.'); + case JSON_ERROR_STATE_MISMATCH: + throw new InvalidParamException('Invalid or malformed JSON.'); + case JSON_ERROR_UTF8: + throw new InvalidParamException('Malformed UTF-8 characters, possibly incorrectly encoded.'); + default: + throw new InvalidParamException('Unknown JSON decoding error.'); + } - /** - * Pre-processes the data before sending it to `json_encode()`. - * @param mixed $data the data to be processed - * @param array $expressions collection of JavaScript expressions - * @param string $expPrefix a prefix internally used to handle JS expressions - * @return mixed the processed data - */ - protected static function processData($data, &$expressions, $expPrefix) - { - if (is_object($data)) { - if ($data instanceof JsExpression) { - $token = "!{[$expPrefix=" . count($expressions) . ']}!'; - $expressions['"' . $token . '"'] = $data->expression; - return $token; - } elseif ($data instanceof \JsonSerializable) { - $data = $data->jsonSerialize(); - } elseif ($data instanceof Arrayable) { - $data = $data->toArray(); - } else { - $result = []; - foreach ($data as $name => $value) { - $result[$name] = $value; - } - $data = $result; - } + return $decode; + } - if ($data === []) { - return new \stdClass(); - } - } + /** + * Pre-processes the data before sending it to `json_encode()`. + * @param mixed $data the data to be processed + * @param array $expressions collection of JavaScript expressions + * @param string $expPrefix a prefix internally used to handle JS expressions + * @return mixed the processed data + */ + protected static function processData($data, &$expressions, $expPrefix) + { + if (is_object($data)) { + if ($data instanceof JsExpression) { + $token = "!{[$expPrefix=" . count($expressions) . ']}!'; + $expressions['"' . $token . '"'] = $data->expression; - if (is_array($data)) { - foreach ($data as $key => $value) { - if (is_array($value) || is_object($value)) { - $data[$key] = static::processData($value, $expressions, $expPrefix); - } - } - } + return $token; + } elseif ($data instanceof \JsonSerializable) { + $data = $data->jsonSerialize(); + } elseif ($data instanceof Arrayable) { + $data = $data->toArray(); + } else { + $result = []; + foreach ($data as $name => $value) { + $result[$name] = $value; + } + $data = $result; + } - return $data; - } + if ($data === []) { + return new \stdClass(); + } + } + + if (is_array($data)) { + foreach ($data as $key => $value) { + if (is_array($value) || is_object($value)) { + $data[$key] = static::processData($value, $expressions, $expPrefix); + } + } + } + + return $data; + } } diff --git a/framework/helpers/BaseMarkdown.php b/framework/helpers/BaseMarkdown.php index c72e07911db..4da6c013878 100644 --- a/framework/helpers/BaseMarkdown.php +++ b/framework/helpers/BaseMarkdown.php @@ -20,81 +20,83 @@ */ class BaseMarkdown { - /** - * @var array a map of markdown flavor names to corresponding parser class configurations. - */ - public static $flavors = [ - 'original' => [ - 'class' => 'cebe\markdown\Markdown', - 'html5' => true, - ], - 'gfm' => [ - 'class' => 'cebe\markdown\GithubMarkdown', - 'html5' => true, - ], - 'gfm-comment' => [ - 'class' => 'cebe\markdown\Markdown', - 'html5' => true, - 'enableNewlines' => true, - ], - ]; - /** - * @var string the markdown flavor to use when none is specified explicitly. - * Defaults to `original`. - * @see $flavors - */ - public static $defaultFlavor = 'original'; + /** + * @var array a map of markdown flavor names to corresponding parser class configurations. + */ + public static $flavors = [ + 'original' => [ + 'class' => 'cebe\markdown\Markdown', + 'html5' => true, + ], + 'gfm' => [ + 'class' => 'cebe\markdown\GithubMarkdown', + 'html5' => true, + ], + 'gfm-comment' => [ + 'class' => 'cebe\markdown\Markdown', + 'html5' => true, + 'enableNewlines' => true, + ], + ]; + /** + * @var string the markdown flavor to use when none is specified explicitly. + * Defaults to `original`. + * @see $flavors + */ + public static $defaultFlavor = 'original'; + /** + * Converts markdown into HTML. + * + * @param string $markdown the markdown text to parse + * @param string $flavor the markdown flavor to use. See [[$flavors]] for available values. + * @return string the parsed HTML output + * @throws \yii\base\InvalidParamException when an undefined flavor is given. + */ + public static function process($markdown, $flavor = 'original') + { + $parser = static::getParser($flavor); - /** - * Converts markdown into HTML. - * - * @param string $markdown the markdown text to parse - * @param string $flavor the markdown flavor to use. See [[$flavors]] for available values. - * @return string the parsed HTML output - * @throws \yii\base\InvalidParamException when an undefined flavor is given. - */ - public static function process($markdown, $flavor = 'original') - { - $parser = static::getParser($flavor); - return $parser->parse($markdown); - } + return $parser->parse($markdown); + } - /** - * Converts markdown into HTML but only parses inline elements. - * - * This can be useful for parsing small comments or description lines. - * - * @param string $markdown the markdown text to parse - * @param string $flavor the markdown flavor to use. See [[$flavors]] for available values. - * @return string the parsed HTML output - * @throws \yii\base\InvalidParamException when an undefined flavor is given. - */ - public static function processParagraph($markdown, $flavor = 'original') - { - $parser = static::getParser($flavor); - return $parser->parseParagraph($markdown); - } + /** + * Converts markdown into HTML but only parses inline elements. + * + * This can be useful for parsing small comments or description lines. + * + * @param string $markdown the markdown text to parse + * @param string $flavor the markdown flavor to use. See [[$flavors]] for available values. + * @return string the parsed HTML output + * @throws \yii\base\InvalidParamException when an undefined flavor is given. + */ + public static function processParagraph($markdown, $flavor = 'original') + { + $parser = static::getParser($flavor); - /** - * @param string $flavor - * @return \cebe\markdown\Parser - * @throws \yii\base\InvalidParamException when an undefined flavor is given. - */ - private static function getParser($flavor) - { - /** @var \cebe\markdown\Markdown $parser */ - if (!isset(static::$flavors[$flavor])) { - throw new InvalidParamException("Markdown flavor '$flavor' is not defined.'"); - } elseif (!is_object($config = static::$flavors[$flavor])) { - $parser = Yii::createObject($config); - if (is_array($config)) { - foreach ($config as $name => $value) { - $parser->{$name} = $value; - } - } - static::$flavors[$flavor] = $parser; - } - return static::$flavors[$flavor]; - } + return $parser->parseParagraph($markdown); + } + + /** + * @param string $flavor + * @return \cebe\markdown\Parser + * @throws \yii\base\InvalidParamException when an undefined flavor is given. + */ + private static function getParser($flavor) + { + /** @var \cebe\markdown\Markdown $parser */ + if (!isset(static::$flavors[$flavor])) { + throw new InvalidParamException("Markdown flavor '$flavor' is not defined.'"); + } elseif (!is_object($config = static::$flavors[$flavor])) { + $parser = Yii::createObject($config); + if (is_array($config)) { + foreach ($config as $name => $value) { + $parser->{$name} = $value; + } + } + static::$flavors[$flavor] = $parser; + } + + return static::$flavors[$flavor]; + } } diff --git a/framework/helpers/BaseSecurity.php b/framework/helpers/BaseSecurity.php index 8750a54c0a0..8a2aca7fe7c 100644 --- a/framework/helpers/BaseSecurity.php +++ b/framework/helpers/BaseSecurity.php @@ -23,337 +23,347 @@ */ class BaseSecurity { - /** - * Uses AES, block size is 128-bit (16 bytes). - */ - const CRYPT_BLOCK_SIZE = 16; - - /** - * Uses AES-192, key size is 192-bit (24 bytes). - */ - const CRYPT_KEY_SIZE = 24; - - /** - * Uses SHA-256. - */ - const DERIVATION_HASH = 'sha256'; - - /** - * Uses 1000 iterations. - */ - const DERIVATION_ITERATIONS = 1000; - - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $password the encryption password - * @return string the encrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see decrypt() - */ - public static function encrypt($data, $password) - { - $module = static::openCryptModule(); - $data = static::addPadding($data); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - $key = static::deriveKey($password, $iv); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $password the decryption password - * @return string the decrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see encrypt() - */ - public static function decrypt($data, $password) - { - if ($data === null) { - return null; - } - $module = static::openCryptModule(); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = StringHelper::byteSubstr($data, 0, $ivSize); - $key = static::deriveKey($password, $iv); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return static::stripPadding($decrypted); - } - - /** - * Adds a padding to the given data (PKCS #7). - * @param string $data the data to pad - * @return string the padded data - */ - protected static function addPadding($data) - { - $pad = self::CRYPT_BLOCK_SIZE - (StringHelper::byteLength($data) % self::CRYPT_BLOCK_SIZE); - return $data . str_repeat(chr($pad), $pad); - } - - /** - * Strips the padding from the given data. - * @param string $data the data to trim - * @return string the trimmed data - */ - protected static function stripPadding($data) - { - $end = StringHelper::byteSubstr($data, -1, null); - $last = ord($end); - $n = StringHelper::byteLength($data) - $last; - if (StringHelper::byteSubstr($data, $n, null) == str_repeat($end, $last)) { - return StringHelper::byteSubstr($data, 0, $n); - } - return false; - } - - /** - * Derives a key from the given password (PBKDF2). - * @param string $password the source password - * @param string $salt the random salt - * @return string the derived key - */ - protected static function deriveKey($password, $salt) - { - if (function_exists('hash_pbkdf2')) { - return hash_pbkdf2(self::DERIVATION_HASH, $password, $salt, self::DERIVATION_ITERATIONS, self::CRYPT_KEY_SIZE, true); - } - $hmac = hash_hmac(self::DERIVATION_HASH, $salt . pack('N', 1), $password, true); - $xorsum = $hmac; - for ($i = 1; $i < self::DERIVATION_ITERATIONS; $i++) { - $hmac = hash_hmac(self::DERIVATION_HASH, $hmac, $password, true); - $xorsum ^= $hmac; - } - return substr($xorsum, 0, self::CRYPT_KEY_SIZE); - } - - /** - * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. - * @param string $data the data to be protected - * @param string $key the secret key to be used for generating hash - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. - * @return string the data prefixed with the keyed hash - * @see validateData() - * @see getSecretKey() - */ - public static function hashData($data, $key, $algorithm = 'sha256') - { - return hash_hmac($algorithm, $data, $key) . $data; - } - - /** - * Validates if the given data is tampered. - * @param string $data the data to be validated. The data must be previously - * generated by [[hashData()]]. - * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. This must be the same - * as the value passed to [[hashData()]] when generating the hash for the data. - * @return string the real data with the hash stripped off. False if the data is tampered. - * @see hashData() - */ - public static function validateData($data, $key, $algorithm = 'sha256') - { - $hashSize = StringHelper::byteLength(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::byteLength($data); - if ($n >= $hashSize) { - $hash = StringHelper::byteSubstr($data, 0, $hashSize); - $data2 = StringHelper::byteSubstr($data, $hashSize, $n - $hashSize); - return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.json" under the application's runtime directory - * so that the same secret key can be returned in future requests. - * @param string $name the name that is associated with the secret key - * @param integer $length the length of the key that should be generated if not exists - * @return string the secret key associated with the specified name - */ - public static function getSecretKey($name, $length = 32) - { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.json'; - if ($keys === null) { - $keys = []; - if (is_file($keyFile)) { - $keys = json_decode(file_get_contents($keyFile), true); - } - } - if (!isset($keys[$name])) { - $keys[$name] = static::generateRandomKey($length); - file_put_contents($keyFile, json_encode($keys)); - } - return $keys[$name]; - } - - /** - * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot. - * @param integer $length the length of the key that should be generated - * @return string the generated random key - */ - public static function generateRandomKey($length = 32) - { - if (function_exists('openssl_random_pseudo_bytes')) { - $key = strtr(base64_encode(openssl_random_pseudo_bytes($length, $strong)), '+/=', '_-.'); - if ($strong) { - return substr($key, 0, $length); - } - } - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; - return substr(str_shuffle(str_repeat($chars, 5)), 0, $length); - } - - /** - * Opens the mcrypt module. - * @return resource the mcrypt module handle. - * @throws InvalidConfigException if mcrypt extension is not installed - * @throws Exception if mcrypt initialization fails - */ - protected static function openCryptModule() - { - if (!extension_loaded('mcrypt')) { - throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); - } - // AES uses a 128-bit block size - $module = @mcrypt_module_open('rijndael-128', '', 'cbc', ''); - if ($module === false) { - throw new Exception('Failed to initialize the mcrypt module.'); - } - return $module; - } - - /** - * Generates a secure hash from a password and a random salt. - * - * The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL). - * Later when a password needs to be validated, the hash can be fetched and passed - * to [[validatePassword()]]. For example, - * - * ~~~ - * // generates the hash (usually done during user registration or when the password is changed) - * $hash = Security::generatePasswordHash($password); - * // ...save $hash in database... - * - * // during login, validate if the password entered is correct using $hash fetched from database - * if (Security::validatePassword($password, $hash) { - * // password is good - * } else { - * // password is bad - * } - * ~~~ - * - * @param string $password The password to be hashed. - * @param integer $cost Cost parameter used by the Blowfish hash algorithm. - * The higher the value of cost, - * the longer it takes to generate the hash and to verify a password against it. Higher cost - * therefore slows down a brute-force attack. For best protection against brute for attacks, - * set it to the highest value that is tolerable on production servers. The time taken to - * compute the hash doubles for every increment by one of $cost. So, for example, if the - * hash takes 1 second to compute when $cost is 14 then then the compute time varies as - * 2^($cost - 14) seconds. - * @throws Exception on bad password parameter or cost parameter - * @return string The password hash string, ASCII and not longer than 64 characters. - * @see validatePassword() - */ - public static function generatePasswordHash($password, $cost = 13) - { - $salt = static::generateSalt($cost); - $hash = crypt($password, $salt); - - if (!is_string($hash) || strlen($hash) < 32) { - throw new Exception('Unknown error occurred while generating hash.'); - } - - return $hash; - } - - /** - * Verifies a password against a hash. - * @param string $password The password to verify. - * @param string $hash The hash to verify the password against. - * @return boolean whether the password is correct. - * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. - * @see generatePasswordHash() - */ - public static function validatePassword($password, $hash) - { - if (!is_string($password) || $password === '') { - throw new InvalidParamException('Password must be a string and cannot be empty.'); - } - - if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { - throw new InvalidParamException('Hash is invalid.'); - } - - $test = crypt($password, $hash); - $n = strlen($test); - if ($n < 32 || $n !== strlen($hash)) { - return false; - } - - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; - } - - /** - * Generates a salt that can be used to generate a password hash. - * - * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function - * requires, for the Blowfish hash algorithm, a salt string in a specific format: - * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters - * from the alphabet "./0-9A-Za-z". - * - * @param integer $cost the cost parameter - * @return string the random salt value. - * @throws InvalidParamException if the cost parameter is not between 4 and 31 - */ - protected static function generateSalt($cost = 13) - { - $cost = (int)$cost; - if ($cost < 4 || $cost > 31) { - throw new InvalidParamException('Cost must be between 4 and 31.'); - } - - // Get 20 * 8bits of random entropy - if (function_exists('openssl_random_pseudo_bytes')) { - // https://github.com/yiisoft/yii2/pull/2422 - $rand = openssl_random_pseudo_bytes(20); - } else { - $rand = ''; - for ($i = 0; $i < 20; ++$i) { - $rand .= chr(mt_rand(0, 255)); - } - } - - // Add the microtime for a little more entropy. - $rand .= microtime(true); - // Mix the bits cryptographically into a 20-byte binary string. - $rand = sha1($rand, true); - // Form the prefix that specifies Blowfish algorithm and cost parameter. - $salt = sprintf("$2y$%02d$", $cost); - // Append the random salt data in the required base64 format. - $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); - return $salt; - } + /** + * Uses AES, block size is 128-bit (16 bytes). + */ + const CRYPT_BLOCK_SIZE = 16; + + /** + * Uses AES-192, key size is 192-bit (24 bytes). + */ + const CRYPT_KEY_SIZE = 24; + + /** + * Uses SHA-256. + */ + const DERIVATION_HASH = 'sha256'; + + /** + * Uses 1000 iterations. + */ + const DERIVATION_ITERATIONS = 1000; + + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $password the encryption password + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $password) + { + $module = static::openCryptModule(); + $data = static::addPadding($data); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + $key = static::deriveKey($password, $iv); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $password the decryption password + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $password) + { + if ($data === null) { + return null; + } + $module = static::openCryptModule(); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::byteSubstr($data, 0, $ivSize); + $key = static::deriveKey($password, $iv); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + + return static::stripPadding($decrypted); + } + + /** + * Adds a padding to the given data (PKCS #7). + * @param string $data the data to pad + * @return string the padded data + */ + protected static function addPadding($data) + { + $pad = self::CRYPT_BLOCK_SIZE - (StringHelper::byteLength($data) % self::CRYPT_BLOCK_SIZE); + + return $data . str_repeat(chr($pad), $pad); + } + + /** + * Strips the padding from the given data. + * @param string $data the data to trim + * @return string the trimmed data + */ + protected static function stripPadding($data) + { + $end = StringHelper::byteSubstr($data, -1, null); + $last = ord($end); + $n = StringHelper::byteLength($data) - $last; + if (StringHelper::byteSubstr($data, $n, null) == str_repeat($end, $last)) { + return StringHelper::byteSubstr($data, 0, $n); + } + + return false; + } + + /** + * Derives a key from the given password (PBKDF2). + * @param string $password the source password + * @param string $salt the random salt + * @return string the derived key + */ + protected static function deriveKey($password, $salt) + { + if (function_exists('hash_pbkdf2')) { + return hash_pbkdf2(self::DERIVATION_HASH, $password, $salt, self::DERIVATION_ITERATIONS, self::CRYPT_KEY_SIZE, true); + } + $hmac = hash_hmac(self::DERIVATION_HASH, $salt . pack('N', 1), $password, true); + $xorsum = $hmac; + for ($i = 1; $i < self::DERIVATION_ITERATIONS; $i++) { + $hmac = hash_hmac(self::DERIVATION_HASH, $hmac, $password, true); + $xorsum ^= $hmac; + } + + return substr($xorsum, 0, self::CRYPT_KEY_SIZE); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::byteLength(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::byteLength($data); + if ($n >= $hashSize) { + $hash = StringHelper::byteSubstr($data, 0, $hashSize); + $data2 = StringHelper::byteSubstr($data, $hashSize, $n - $hashSize); + + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.json" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.json'; + if ($keys === null) { + $keys = []; + if (is_file($keyFile)) { + $keys = json_decode(file_get_contents($keyFile), true); + } + } + if (!isset($keys[$name])) { + $keys[$name] = static::generateRandomKey($length); + file_put_contents($keyFile, json_encode($keys)); + } + + return $keys[$name]; + } + + /** + * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot. + * @param integer $length the length of the key that should be generated + * @return string the generated random key + */ + public static function generateRandomKey($length = 32) + { + if (function_exists('openssl_random_pseudo_bytes')) { + $key = strtr(base64_encode(openssl_random_pseudo_bytes($length, $strong)), '+/=', '_-.'); + if ($strong) { + return substr($key, 0, $length); + } + } + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; + + return substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + } + + /** + * Opens the mcrypt module. + * @return resource the mcrypt module handle. + * @throws InvalidConfigException if mcrypt extension is not installed + * @throws Exception if mcrypt initialization fails + */ + protected static function openCryptModule() + { + if (!extension_loaded('mcrypt')) { + throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); + } + // AES uses a 128-bit block size + $module = @mcrypt_module_open('rijndael-128', '', 'cbc', ''); + if ($module === false) { + throw new Exception('Failed to initialize the mcrypt module.'); + } + + return $module; + } + + /** + * Generates a secure hash from a password and a random salt. + * + * The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL). + * Later when a password needs to be validated, the hash can be fetched and passed + * to [[validatePassword()]]. For example, + * + * ~~~ + * // generates the hash (usually done during user registration or when the password is changed) + * $hash = Security::generatePasswordHash($password); + * // ...save $hash in database... + * + * // during login, validate if the password entered is correct using $hash fetched from database + * if (Security::validatePassword($password, $hash) { + * // password is good + * } else { + * // password is bad + * } + * ~~~ + * + * @param string $password The password to be hashed. + * @param integer $cost Cost parameter used by the Blowfish hash algorithm. + * The higher the value of cost, + * the longer it takes to generate the hash and to verify a password against it. Higher cost + * therefore slows down a brute-force attack. For best protection against brute for attacks, + * set it to the highest value that is tolerable on production servers. The time taken to + * compute the hash doubles for every increment by one of $cost. So, for example, if the + * hash takes 1 second to compute when $cost is 14 then then the compute time varies as + * 2^($cost - 14) seconds. + * @throws Exception on bad password parameter or cost parameter + * @return string The password hash string, ASCII and not longer than 64 characters. + * @see validatePassword() + */ + public static function generatePasswordHash($password, $cost = 13) + { + $salt = static::generateSalt($cost); + $hash = crypt($password, $salt); + + if (!is_string($hash) || strlen($hash) < 32) { + throw new Exception('Unknown error occurred while generating hash.'); + } + + return $hash; + } + + /** + * Verifies a password against a hash. + * @param string $password The password to verify. + * @param string $hash The hash to verify the password against. + * @return boolean whether the password is correct. + * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. + * @see generatePasswordHash() + */ + public static function validatePassword($password, $hash) + { + if (!is_string($password) || $password === '') { + throw new InvalidParamException('Password must be a string and cannot be empty.'); + } + + if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if ($n < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 31 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int) $cost; + if ($cost < 4 || $cost > 31) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of random entropy + if (function_exists('openssl_random_pseudo_bytes')) { + // https://github.com/yiisoft/yii2/pull/2422 + $rand = openssl_random_pseudo_bytes(20); + } else { + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + } + + // Add the microtime for a little more entropy. + $rand .= microtime(true); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + + return $salt; + } } diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index 06fbed66d68..34432cadf6b 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -18,72 +18,73 @@ */ class BaseStringHelper { - /** - * Returns the number of bytes in the given string. - * This method ensures the string is treated as a byte array by using `mb_strlen()`. - * @param string $string the string being measured for length - * @return integer the number of bytes in the given string. - */ - public static function byteLength($string) - { - return mb_strlen($string, '8bit'); - } + /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array by using `mb_strlen()`. + * @param string $string the string being measured for length + * @return integer the number of bytes in the given string. + */ + public static function byteLength($string) + { + return mb_strlen($string, '8bit'); + } - /** - * Returns the portion of string specified by the start and length parameters. - * This method ensures the string is treated as a byte array by using `mb_substr()`. - * @param string $string the input string. Must be one character or longer. - * @param integer $start the starting position - * @param integer $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - * @see http://www.php.net/manual/en/function.substr.php - */ - public static function byteSubstr($string, $start, $length) - { - return mb_substr($string, $start, $length, '8bit'); - } + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array by using `mb_substr()`. + * @param string $string the input string. Must be one character or longer. + * @param integer $start the starting position + * @param integer $length the desired portion length + * @return string the extracted part of string, or FALSE on failure or an empty string. + * @see http://www.php.net/manual/en/function.substr.php + */ + public static function byteSubstr($string, $start, $length) + { + return mb_substr($string, $start, $length, '8bit'); + } - /** - * Returns the trailing name component of a path. - * This method is similar to the php function `basename()` except that it will - * treat both \ and / as directory separators, independent of the operating system. - * This method was mainly created to work on php namespaces. When working with real - * file paths, php's `basename()` should work fine for you. - * Note: this method is not aware of the actual filesystem, or path components such as "..". - * - * @param string $path A path string. - * @param string $suffix If the name component ends in suffix this will also be cut off. - * @return string the trailing name component of the given path. - * @see http://www.php.net/manual/en/function.basename.php - */ - public static function basename($path, $suffix = '') - { - if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) == $suffix) { - $path = mb_substr($path, 0, -$len); - } - $path = rtrim(str_replace('\\', '/', $path), '/\\'); - if (($pos = mb_strrpos($path, '/')) !== false) { - return mb_substr($path, $pos + 1); - } - return $path; - } + /** + * Returns the trailing name component of a path. + * This method is similar to the php function `basename()` except that it will + * treat both \ and / as directory separators, independent of the operating system. + * This method was mainly created to work on php namespaces. When working with real + * file paths, php's `basename()` should work fine for you. + * Note: this method is not aware of the actual filesystem, or path components such as "..". + * + * @param string $path A path string. + * @param string $suffix If the name component ends in suffix this will also be cut off. + * @return string the trailing name component of the given path. + * @see http://www.php.net/manual/en/function.basename.php + */ + public static function basename($path, $suffix = '') + { + if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) == $suffix) { + $path = mb_substr($path, 0, -$len); + } + $path = rtrim(str_replace('\\', '/', $path), '/\\'); + if (($pos = mb_strrpos($path, '/')) !== false) { + return mb_substr($path, $pos + 1); + } - /** - * Returns parent directory's path. - * This method is similar to `dirname()` except that it will treat - * both \ and / as directory separators, independent of the operating system. - * - * @param string $path A path string. - * @return string the parent directory's path. - * @see http://www.php.net/manual/en/function.basename.php - */ - public static function dirname($path) - { - $pos = mb_strrpos(str_replace('\\', '/', $path), '/'); - if ($pos !== false) { - return mb_substr($path, 0, $pos); - } else { - return ''; - } - } + return $path; + } + + /** + * Returns parent directory's path. + * This method is similar to `dirname()` except that it will treat + * both \ and / as directory separators, independent of the operating system. + * + * @param string $path A path string. + * @return string the parent directory's path. + * @see http://www.php.net/manual/en/function.basename.php + */ + public static function dirname($path) + { + $pos = mb_strrpos(str_replace('\\', '/', $path), '/'); + if ($pos !== false) { + return mb_substr($path, 0, $pos); + } else { + return ''; + } + } } diff --git a/framework/helpers/BaseUrl.php b/framework/helpers/BaseUrl.php index 8712a655c34..ffc95095c8e 100644 --- a/framework/helpers/BaseUrl.php +++ b/framework/helpers/BaseUrl.php @@ -21,207 +21,212 @@ */ class BaseUrl { - /** - * Returns URL for a route. - * - * @param array|string $route route as a string or route and parameters in form of - * `['route', 'param1' => 'value1', 'param2' => 'value2']`. - * - * If there is a controller running, relative routes are recognized: - * - * - If the route is an empty string, the current [[\yii\web\Controller::route|route]] will be used; - * - If the route contains no slashes at all, it is considered to be an action ID - * of the current controller and will be prepended with [[\yii\web\Controller::uniqueId]]; - * - If the route has no leading slash, it is considered to be a route relative - * to the current module and will be prepended with the module's uniqueId. - * - * In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. - * - * @param boolean|string $scheme URI scheme to use: - * - * - `false`: relative URL. Default behavior. - * - `true`: absolute URL with the current scheme. - * - string: absolute URL with string value used as scheme. - * - * @return string the URL for the route - * @throws InvalidParamException if the parameter is invalid. - */ - public static function toRoute($route, $scheme = false) - { - $route = (array)$route; - if (Yii::$app->controller instanceof Controller) { - $route[0] = static::getNormalizedRoute($route[0]); - } - if ($scheme) { - if ($scheme === true) { - $scheme = null; - } - $url = Yii::$app->getUrlManager()->createAbsoluteUrl($route, $scheme); - } else { - $url = Yii::$app->getUrlManager()->createUrl($route); - } - return $url; - } - - /** - * Normalizes route and makes it suitable for UrlManager. Absolute routes are staying as is - * while relative routes are converted to absolute ones. - * - * A relative route is a route without a leading slash, such as "view", "post/view". - * - * - If the route is an empty string, the current [[\yii\web\Controller::route|route]] will be used; - * - If the route contains no slashes at all, it is considered to be an action ID - * of the current controller and will be prepended with [[\yii\web\Controller::uniqueId]]; - * - If the route has no leading slash, it is considered to be a route relative - * to the current module and will be prepended with the module's uniqueId. - * - * @param string $route the route. This can be either an absolute route or a relative route. - * @return string normalized route suitable for UrlManager - */ - private static function getNormalizedRoute($route) - { - if (strpos($route, '/') === false) { - // empty or an action ID - $route = $route === '' ? Yii::$app->controller->getRoute() : Yii::$app->controller->getUniqueId() . '/' . $route; - } elseif ($route[0] !== '/') { - // relative to module - $route = ltrim(Yii::$app->controller->module->getUniqueId() . '/' . $route, '/'); - } - return $route; - } - - /** - * Creates a URL specified by the input parameter. - * - * If the input parameter is - * - * - an array: the first array element is considered a route, while the rest of the name-value - * pairs are treated as the parameters to be used for URL creation using [[toRoute()]]. - * For example: `['post/index', 'page' => 2]`, `['index']`. - * In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. - * - an empty string: the currently requested URL will be returned; - * - a non-empty string: it will first be processed by [[Yii::getAlias()]]. If the result - * is an absolute URL, it will be returned either without any change or, if scheme was specified, with scheme - * replaced; Otherwise, the result will be prefixed with [[\yii\web\Request::baseUrl]] and returned. - - * - * @param array|string $url the parameter to be used to generate a valid URL - * @param boolean|string $scheme URI scheme to use: - * - * - `false`: relative URL. Default behavior. - * - `true`: absolute URL with the current scheme. - * - string: absolute URL with string value used as scheme. - * - * @return string the normalized URL - * @throws InvalidParamException if the parameter is invalid. - */ - public static function to($url = '', $scheme = false) - { - if (is_array($url)) { - return static::toRoute($url, $scheme); - } elseif ($url === '') { - if ($scheme) { - $url = Yii::$app->getRequest()->getAbsoluteUrl(); - } else { - $url = Yii::$app->getRequest()->getUrl(); - } - } else { - $url = Yii::getAlias($url); - if (strpos($url, '://') === false) { - if ($url === '' || ($url[0] !== '/' && $url[0] !== '#' && strncmp($url, './', 2))) { - $url = Yii::$app->getRequest()->getBaseUrl() . '/' . $url; - } - if ($scheme) { - $url = Yii::$app->getRequest()->getHostInfo() . $url; - } - } - } - if ($scheme && $scheme !== true) { - $pos = strpos($url, '://'); - if ($pos !== false) { - $url = $scheme . substr($url, $pos); - } - } - return $url; - } - - /** - * Remembers the specified URL so that it can be later fetched back. - * - * @param string $url URL to remember. Default is the currently requested URL. - * @param string $name Name to use to remember URL. Defaults to [[\yii\web\User::returnUrlParam]]. - * @see previous() - */ - public static function remember($url = '', $name = null) - { - if ($url === '') { - $url = Yii::$app->getRequest()->getUrl(); - } - - if ($name === null) { - Yii::$app->getUser()->setReturnUrl($url); - } else { - Yii::$app->getSession()->set($name, $url); - } - } - - /** - * Returns the URL previously [[remember()|remembered]]. - * - * @param string $name Name used to remember URL. Defaults to [[\yii\web\User::returnUrlParam]]. - * @return string URL, or null if no such URL was remembered before. - * @see remember() - */ - public static function previous($name = null) - { - if ($name === null) { - return Yii::$app->getUser()->getReturnUrl(); - } else { - return Yii::$app->getSession()->get($name); - } - } - - /** - * Returns the canonical URL of the currently requested page. - * The canonical URL is constructed using current controller's [[yii\web\Controller::route]] and - * [[yii\web\Controller::actionParams]]. You may use the following code in the layout view to add a link tag - * about canonical URL: - * - * ```php - * $this->registerLinkTag(['rel' => 'canonical', 'href' => Url::canonical()]); - * ``` - * - * @return string the canonical URL of the currently requested page - */ - public static function canonical() - { - $params = Yii::$app->controller->actionParams; - $params[0] = Yii::$app->controller->getRoute(); - return Yii::$app->getUrlManager()->createAbsoluteUrl($params); - } - - /** - * Returns the home URL. - * - * @param boolean|string $scheme URI scheme to use: - * - * - `false`: relative URL. Default behavior. - * - `true`: absolute URL with the current scheme. - * - string: absolute URL with string value used as scheme. - * - * @return string home URL - */ - public static function home($scheme = false) - { - if ($scheme) { - $url = Yii::$app->getRequest()->getHostInfo() . Yii::$app->getHomeUrl(); - if ($scheme !== true) { - $pos = strpos($url, '://'); - $url = $scheme . substr($url, $pos); - } - } else { - $url = Yii::$app->getHomeUrl(); - } - return $url; - } + /** + * Returns URL for a route. + * + * @param array|string $route route as a string or route and parameters in form of + * `['route', 'param1' => 'value1', 'param2' => 'value2']`. + * + * If there is a controller running, relative routes are recognized: + * + * - If the route is an empty string, the current [[\yii\web\Controller::route|route]] will be used; + * - If the route contains no slashes at all, it is considered to be an action ID + * of the current controller and will be prepended with [[\yii\web\Controller::uniqueId]]; + * - If the route has no leading slash, it is considered to be a route relative + * to the current module and will be prepended with the module's uniqueId. + * + * In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. + * + * @param boolean|string $scheme URI scheme to use: + * + * - `false`: relative URL. Default behavior. + * - `true`: absolute URL with the current scheme. + * - string: absolute URL with string value used as scheme. + * + * @return string the URL for the route + * @throws InvalidParamException if the parameter is invalid. + */ + public static function toRoute($route, $scheme = false) + { + $route = (array) $route; + if (Yii::$app->controller instanceof Controller) { + $route[0] = static::getNormalizedRoute($route[0]); + } + if ($scheme) { + if ($scheme === true) { + $scheme = null; + } + $url = Yii::$app->getUrlManager()->createAbsoluteUrl($route, $scheme); + } else { + $url = Yii::$app->getUrlManager()->createUrl($route); + } + + return $url; + } + + /** + * Normalizes route and makes it suitable for UrlManager. Absolute routes are staying as is + * while relative routes are converted to absolute ones. + * + * A relative route is a route without a leading slash, such as "view", "post/view". + * + * - If the route is an empty string, the current [[\yii\web\Controller::route|route]] will be used; + * - If the route contains no slashes at all, it is considered to be an action ID + * of the current controller and will be prepended with [[\yii\web\Controller::uniqueId]]; + * - If the route has no leading slash, it is considered to be a route relative + * to the current module and will be prepended with the module's uniqueId. + * + * @param string $route the route. This can be either an absolute route or a relative route. + * @return string normalized route suitable for UrlManager + */ + private static function getNormalizedRoute($route) + { + if (strpos($route, '/') === false) { + // empty or an action ID + $route = $route === '' ? Yii::$app->controller->getRoute() : Yii::$app->controller->getUniqueId() . '/' . $route; + } elseif ($route[0] !== '/') { + // relative to module + $route = ltrim(Yii::$app->controller->module->getUniqueId() . '/' . $route, '/'); + } + + return $route; + } + + /** + * Creates a URL specified by the input parameter. + * + * If the input parameter is + * + * - an array: the first array element is considered a route, while the rest of the name-value + * pairs are treated as the parameters to be used for URL creation using [[toRoute()]]. + * For example: `['post/index', 'page' => 2]`, `['index']`. + * In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. + * - an empty string: the currently requested URL will be returned; + * - a non-empty string: it will first be processed by [[Yii::getAlias()]]. If the result + * is an absolute URL, it will be returned either without any change or, if scheme was specified, with scheme + * replaced; Otherwise, the result will be prefixed with [[\yii\web\Request::baseUrl]] and returned. + + * + * @param array|string $url the parameter to be used to generate a valid URL + * @param boolean|string $scheme URI scheme to use: + * + * - `false`: relative URL. Default behavior. + * - `true`: absolute URL with the current scheme. + * - string: absolute URL with string value used as scheme. + * + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function to($url = '', $scheme = false) + { + if (is_array($url)) { + return static::toRoute($url, $scheme); + } elseif ($url === '') { + if ($scheme) { + $url = Yii::$app->getRequest()->getAbsoluteUrl(); + } else { + $url = Yii::$app->getRequest()->getUrl(); + } + } else { + $url = Yii::getAlias($url); + if (strpos($url, '://') === false) { + if ($url === '' || ($url[0] !== '/' && $url[0] !== '#' && strncmp($url, './', 2))) { + $url = Yii::$app->getRequest()->getBaseUrl() . '/' . $url; + } + if ($scheme) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + } + } + if ($scheme && $scheme !== true) { + $pos = strpos($url, '://'); + if ($pos !== false) { + $url = $scheme . substr($url, $pos); + } + } + + return $url; + } + + /** + * Remembers the specified URL so that it can be later fetched back. + * + * @param string $url URL to remember. Default is the currently requested URL. + * @param string $name Name to use to remember URL. Defaults to [[\yii\web\User::returnUrlParam]]. + * @see previous() + */ + public static function remember($url = '', $name = null) + { + if ($url === '') { + $url = Yii::$app->getRequest()->getUrl(); + } + + if ($name === null) { + Yii::$app->getUser()->setReturnUrl($url); + } else { + Yii::$app->getSession()->set($name, $url); + } + } + + /** + * Returns the URL previously [[remember()|remembered]]. + * + * @param string $name Name used to remember URL. Defaults to [[\yii\web\User::returnUrlParam]]. + * @return string URL, or null if no such URL was remembered before. + * @see remember() + */ + public static function previous($name = null) + { + if ($name === null) { + return Yii::$app->getUser()->getReturnUrl(); + } else { + return Yii::$app->getSession()->get($name); + } + } + + /** + * Returns the canonical URL of the currently requested page. + * The canonical URL is constructed using current controller's [[yii\web\Controller::route]] and + * [[yii\web\Controller::actionParams]]. You may use the following code in the layout view to add a link tag + * about canonical URL: + * + * ```php + * $this->registerLinkTag(['rel' => 'canonical', 'href' => Url::canonical()]); + * ``` + * + * @return string the canonical URL of the currently requested page + */ + public static function canonical() + { + $params = Yii::$app->controller->actionParams; + $params[0] = Yii::$app->controller->getRoute(); + + return Yii::$app->getUrlManager()->createAbsoluteUrl($params); + } + + /** + * Returns the home URL. + * + * @param boolean|string $scheme URI scheme to use: + * + * - `false`: relative URL. Default behavior. + * - `true`: absolute URL with the current scheme. + * - string: absolute URL with string value used as scheme. + * + * @return string home URL + */ + public static function home($scheme = false) + { + if ($scheme) { + $url = Yii::$app->getRequest()->getHostInfo() . Yii::$app->getHomeUrl(); + if ($scheme !== true) { + $pos = strpos($url, '://'); + $url = $scheme . substr($url, $pos); + } + } else { + $url = Yii::$app->getHomeUrl(); + } + + return $url; + } } diff --git a/framework/helpers/BaseVarDumper.php b/framework/helpers/BaseVarDumper.php index 36b739ccfbd..76101e8d391 100644 --- a/framework/helpers/BaseVarDumper.php +++ b/framework/helpers/BaseVarDumper.php @@ -18,109 +18,110 @@ */ class BaseVarDumper { - private static $_objects; - private static $_output; - private static $_depth; + private static $_objects; + private static $_output; + private static $_depth; - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - */ - public static function dump($var, $depth = 10, $highlight = false) - { - echo static::dumpAsString($var, $depth, $highlight); - } + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + */ + public static function dump($var, $depth = 10, $highlight = false) + { + echo static::dumpAsString($var, $depth, $highlight); + } - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - * @return string the string representation of the variable - */ - public static function dumpAsString($var, $depth = 10, $highlight = false) - { - self::$_output = ''; - self::$_objects = []; - self::$_depth = $depth; - self::dumpInternal($var, 0); - if ($highlight) { - $result = highlight_string("/', '', $result, 1); - } - return self::$_output; - } + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + * @return string the string representation of the variable + */ + public static function dumpAsString($var, $depth = 10, $highlight = false) + { + self::$_output = ''; + self::$_objects = []; + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("/', '', $result, 1); + } - /** - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var, $level) - { - switch (gettype($var)) { - case 'boolean': - self::$_output .= $var ? 'true' : 'false'; - break; - case 'integer': - self::$_output .= "$var"; - break; - case 'double': - self::$_output .= "$var"; - break; - case 'string': - self::$_output .= "'" . addslashes($var) . "'"; - break; - case 'resource': - self::$_output .= '{resource}'; - break; - case 'NULL': - self::$_output .= "null"; - break; - case 'unknown type': - self::$_output .= '{unknown}'; - break; - case 'array': - if (self::$_depth <= $level) { - self::$_output .= '[...]'; - } elseif (empty($var)) { - self::$_output .= '[]'; - } else { - $keys = array_keys($var); - $spaces = str_repeat(' ', $level * 4); - self::$_output .= '['; - foreach ($keys as $key) { - self::$_output .= "\n" . $spaces . ' '; - self::dumpInternal($key, 0); - self::$_output .= ' => '; - self::dumpInternal($var[$key], $level + 1); - } - self::$_output .= "\n" . $spaces . ']'; - } - break; - case 'object': - if (($id = array_search($var, self::$_objects, true)) !== false) { - self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; - } elseif (self::$_depth <= $level) { - self::$_output .= get_class($var) . '(...)'; - } else { - $id = array_push(self::$_objects, $var); - $className = get_class($var); - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "$className#$id\n" . $spaces . '('; - foreach ((array)$var as $key => $value) { - $keyDisplay = strtr(trim($key), ["\0" => ':']); - self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; - self::dumpInternal($value, $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - } - } + return self::$_output; + } + + /** + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + if (self::$_depth <= $level) { + self::$_output .= '[...]'; + } elseif (empty($var)) { + self::$_output .= '[]'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= '['; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); + } + self::$_output .= "\n" . $spaces . ']'; + } + break; + case 'object': + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = array_push(self::$_objects, $var); + $className = get_class($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ((array) $var as $key => $value) { + $keyDisplay = strtr(trim($key), ["\0" => ':']); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + } + } } diff --git a/framework/helpers/Url.php b/framework/helpers/Url.php index 87e29c4833c..b2998c7bdd6 100644 --- a/framework/helpers/Url.php +++ b/framework/helpers/Url.php @@ -16,4 +16,3 @@ class Url extends BaseUrl { } - \ No newline at end of file diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php index 4e9f61ac470..7f95e0b0f6a 100644 --- a/framework/helpers/mimeTypes.php +++ b/framework/helpers/mimeTypes.php @@ -12,176 +12,176 @@ */ return [ - 'ai' => 'application/postscript', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'anx' => 'application/annodex', - 'asc' => 'text/plain', - 'au' => 'audio/basic', - 'avi' => 'video/x-msvideo', - 'axa' => 'audio/annodex', - 'axv' => 'video/annodex', - 'bcpio' => 'application/x-bcpio', - 'bin' => 'application/octet-stream', - 'bmp' => 'image/bmp', - 'c' => 'text/plain', - 'cc' => 'text/plain', - 'ccad' => 'application/clariscad', - 'cdf' => 'application/x-netcdf', - 'class' => 'application/octet-stream', - 'cpio' => 'application/x-cpio', - 'cpt' => 'application/mac-compactpro', - 'csh' => 'application/x-csh', - 'css' => 'text/css', - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dms' => 'application/octet-stream', - 'doc' => 'application/msword', - 'drw' => 'application/drafting', - 'dvi' => 'application/x-dvi', - 'dwg' => 'application/acad', - 'dxf' => 'application/dxf', - 'dxr' => 'application/x-director', - 'eps' => 'application/postscript', - 'etx' => 'text/x-setext', - 'exe' => 'application/octet-stream', - 'ez' => 'application/andrew-inset', - 'f' => 'text/plain', - 'f90' => 'text/plain', - 'flac' => 'audio/flac', - 'fli' => 'video/x-fli', - 'flv' => 'video/x-flv', - 'gif' => 'image/gif', - 'gtar' => 'application/x-gtar', - 'gz' => 'application/x-gzip', - 'h' => 'text/plain', - 'hdf' => 'application/x-hdf', - 'hh' => 'text/plain', - 'hqx' => 'application/mac-binhex40', - 'htm' => 'text/html', - 'html' => 'text/html', - 'ice' => 'x-conference/x-cooltalk', - 'ief' => 'image/ief', - 'iges' => 'model/iges', - 'igs' => 'model/iges', - 'ips' => 'application/x-ipscript', - 'ipx' => 'application/x-ipix', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'js' => 'application/x-javascript', - 'kar' => 'audio/midi', - 'latex' => 'application/x-latex', - 'lha' => 'application/octet-stream', - 'lsp' => 'application/x-lisp', - 'lzh' => 'application/octet-stream', - 'm' => 'text/plain', - 'man' => 'application/x-troff-man', - 'me' => 'application/x-troff-me', - 'mesh' => 'model/mesh', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mif' => 'application/vnd.mif', - 'mime' => 'www/mime', - 'mov' => 'video/quicktime', - 'movie' => 'video/x-sgi-movie', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mpga' => 'audio/mpeg', - 'ms' => 'application/x-troff-ms', - 'msh' => 'model/mesh', - 'nc' => 'application/x-netcdf', - 'oga' => 'audio/ogg', - 'ogg' => 'audio/ogg', - 'ogv' => 'video/ogg', - 'ogx' => 'application/ogg', - 'oda' => 'application/oda', - 'pbm' => 'image/x-portable-bitmap', - 'pdb' => 'chemical/x-pdb', - 'pdf' => 'application/pdf', - 'pgm' => 'image/x-portable-graymap', - 'pgn' => 'application/x-chess-pgn', - 'png' => 'image/png', - 'pnm' => 'image/x-portable-anymap', - 'pot' => 'application/mspowerpoint', - 'ppm' => 'image/x-portable-pixmap', - 'pps' => 'application/mspowerpoint', - 'ppt' => 'application/mspowerpoint', - 'ppz' => 'application/mspowerpoint', - 'pre' => 'application/x-freelance', - 'prt' => 'application/pro_eng', - 'ps' => 'application/postscript', - 'qt' => 'video/quicktime', - 'ra' => 'audio/x-realaudio', - 'ram' => 'audio/x-pn-realaudio', - 'ras' => 'image/cmu-raster', - 'rgb' => 'image/x-rgb', - 'rm' => 'audio/x-pn-realaudio', - 'roff' => 'application/x-troff', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'rtf' => 'text/rtf', - 'rtx' => 'text/richtext', - 'scm' => 'application/x-lotusscreencam', - 'set' => 'application/set', - 'sgm' => 'text/sgml', - 'sgml' => 'text/sgml', - 'sh' => 'application/x-sh', - 'shar' => 'application/x-shar', - 'silo' => 'model/mesh', - 'sit' => 'application/x-stuffit', - 'skd' => 'application/x-koan', - 'skm' => 'application/x-koan', - 'skp' => 'application/x-koan', - 'skt' => 'application/x-koan', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'snd' => 'audio/basic', - 'sol' => 'application/solids', - 'spl' => 'application/x-futuresplash', - 'spx' => 'audio/ogg', - 'src' => 'application/x-wais-source', - 'step' => 'application/STEP', - 'stl' => 'application/SLA', - 'stp' => 'application/STEP', - 'sv4cpio' => 'application/x-sv4cpio', - 'sv4crc' => 'application/x-sv4crc', - 'swf' => 'application/x-shockwave-flash', - 't' => 'application/x-troff', - 'tar' => 'application/x-tar', - 'tcl' => 'application/x-tcl', - 'tex' => 'application/x-tex', - 'texi' => 'application/x-texinfo', - 'texinfo' => 'application/x-texinfo', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'tr' => 'application/x-troff', - 'tsi' => 'audio/TSP-audio', - 'tsp' => 'application/dsptype', - 'tsv' => 'text/tab-separated-values', - 'txt' => 'text/plain', - 'unv' => 'application/i-deas', - 'ustar' => 'application/x-ustar', - 'vcd' => 'application/x-cdlink', - 'vda' => 'application/vda', - 'viv' => 'video/vnd.vivo', - 'vivo' => 'video/vnd.vivo', - 'vrml' => 'model/vrml', - 'wav' => 'audio/x-wav', - 'wrl' => 'model/vrml', - 'xbm' => 'image/x-xbitmap', - 'xlc' => 'application/vnd.ms-excel', - 'xll' => 'application/vnd.ms-excel', - 'xlm' => 'application/vnd.ms-excel', - 'xls' => 'application/vnd.ms-excel', - 'xlw' => 'application/vnd.ms-excel', - 'xml' => 'application/xml', - 'xpm' => 'image/x-xpixmap', - 'xspf' => 'application/xspf+xml', - 'xwd' => 'image/x-xwindowdump', - 'xyz' => 'chemical/x-pdb', - 'zip' => 'application/zip', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'anx' => 'application/annodex', + 'asc' => 'text/plain', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'axa' => 'audio/annodex', + 'axv' => 'video/annodex', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/octet-stream', + 'bmp' => 'image/bmp', + 'c' => 'text/plain', + 'cc' => 'text/plain', + 'ccad' => 'application/clariscad', + 'cdf' => 'application/x-netcdf', + 'class' => 'application/octet-stream', + 'cpio' => 'application/x-cpio', + 'cpt' => 'application/mac-compactpro', + 'csh' => 'application/x-csh', + 'css' => 'text/css', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'drw' => 'application/drafting', + 'dvi' => 'application/x-dvi', + 'dwg' => 'application/acad', + 'dxf' => 'application/dxf', + 'dxr' => 'application/x-director', + 'eps' => 'application/postscript', + 'etx' => 'text/x-setext', + 'exe' => 'application/octet-stream', + 'ez' => 'application/andrew-inset', + 'f' => 'text/plain', + 'f90' => 'text/plain', + 'flac' => 'audio/flac', + 'fli' => 'video/x-fli', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'h' => 'text/plain', + 'hdf' => 'application/x-hdf', + 'hh' => 'text/plain', + 'hqx' => 'application/mac-binhex40', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ice' => 'x-conference/x-cooltalk', + 'ief' => 'image/ief', + 'iges' => 'model/iges', + 'igs' => 'model/iges', + 'ips' => 'application/x-ipscript', + 'ipx' => 'application/x-ipix', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'application/x-javascript', + 'kar' => 'audio/midi', + 'latex' => 'application/x-latex', + 'lha' => 'application/octet-stream', + 'lsp' => 'application/x-lisp', + 'lzh' => 'application/octet-stream', + 'm' => 'text/plain', + 'man' => 'application/x-troff-man', + 'me' => 'application/x-troff-me', + 'mesh' => 'model/mesh', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mime' => 'www/mime', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'ms' => 'application/x-troff-ms', + 'msh' => 'model/mesh', + 'nc' => 'application/x-netcdf', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'oda' => 'application/oda', + 'pbm' => 'image/x-portable-bitmap', + 'pdb' => 'chemical/x-pdb', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'pot' => 'application/mspowerpoint', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/mspowerpoint', + 'ppt' => 'application/mspowerpoint', + 'ppz' => 'application/mspowerpoint', + 'pre' => 'application/x-freelance', + 'prt' => 'application/pro_eng', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'ras' => 'image/cmu-raster', + 'rgb' => 'image/x-rgb', + 'rm' => 'audio/x-pn-realaudio', + 'roff' => 'application/x-troff', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'scm' => 'application/x-lotusscreencam', + 'set' => 'application/set', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'silo' => 'model/mesh', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'snd' => 'audio/basic', + 'sol' => 'application/solids', + 'spl' => 'application/x-futuresplash', + 'spx' => 'audio/ogg', + 'src' => 'application/x-wais-source', + 'step' => 'application/STEP', + 'stl' => 'application/SLA', + 'stp' => 'application/STEP', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tr' => 'application/x-troff', + 'tsi' => 'audio/TSP-audio', + 'tsp' => 'application/dsptype', + 'tsv' => 'text/tab-separated-values', + 'txt' => 'text/plain', + 'unv' => 'application/i-deas', + 'ustar' => 'application/x-ustar', + 'vcd' => 'application/x-cdlink', + 'vda' => 'application/vda', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vnd.vivo', + 'vrml' => 'model/vrml', + 'wav' => 'audio/x-wav', + 'wrl' => 'model/vrml', + 'xbm' => 'image/x-xbitmap', + 'xlc' => 'application/vnd.ms-excel', + 'xll' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlw' => 'application/vnd.ms-excel', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xspf' => 'application/xspf+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-pdb', + 'zip' => 'application/zip', ]; diff --git a/framework/i18n/DbMessageSource.php b/framework/i18n/DbMessageSource.php index 31fca124eab..9b323682550 100644 --- a/framework/i18n/DbMessageSource.php +++ b/framework/i18n/DbMessageSource.php @@ -46,126 +46,128 @@ */ class DbMessageSource extends MessageSource { - /** - * Prefix which would be used when generating cache key. - */ - const CACHE_KEY_PREFIX = 'DbMessageSource'; + /** + * Prefix which would be used when generating cache key. + */ + const CACHE_KEY_PREFIX = 'DbMessageSource'; - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbMessageSource object is created, if you want to change this property, you should only assign - * it with a DB connection object. - */ - public $db = 'db'; - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * The messages data will be cached using this cache object. Note, this property has meaning only - * in case [[cachingDuration]] set to non-zero value. - * After the DbMessageSource object is created, if you want to change this property, you should only assign - * it with a cache object. - */ - public $cache = 'cache'; - /** - * @var string the name of the source message table. - */ - public $sourceMessageTable = '{{%source_message}}'; - /** - * @var string the name of the translated message table. - */ - public $messageTable = '{{%message}}'; - /** - * @var integer the time in seconds that the messages can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - * @see enableCaching - */ - public $cachingDuration = 0; - /** - * @var boolean whether to enable caching translated messages - */ - public $enableCaching = false; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbMessageSource object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * The messages data will be cached using this cache object. Note, this property has meaning only + * in case [[cachingDuration]] set to non-zero value. + * After the DbMessageSource object is created, if you want to change this property, you should only assign + * it with a cache object. + */ + public $cache = 'cache'; + /** + * @var string the name of the source message table. + */ + public $sourceMessageTable = '{{%source_message}}'; + /** + * @var string the name of the translated message table. + */ + public $messageTable = '{{%message}}'; + /** + * @var integer the time in seconds that the messages can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + * @see enableCaching + */ + public $cachingDuration = 0; + /** + * @var boolean whether to enable caching translated messages + */ + public $enableCaching = false; - /** - * Initializes the DbMessageSource component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * Configured [[cache]] component would also be initialized. - * @throws InvalidConfigException if [[db]] is invalid or [[cache]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbMessageSource::db must be either a DB connection instance or the application component ID of a DB connection."); - } - if ($this->enableCaching) { - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } - if (!$this->cache instanceof Cache) { - throw new InvalidConfigException("DbMessageSource::cache must be either a cache object or the application component ID of the cache object."); - } - } - } + /** + * Initializes the DbMessageSource component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * Configured [[cache]] component would also be initialized. + * @throws InvalidConfigException if [[db]] is invalid or [[cache]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbMessageSource::db must be either a DB connection instance or the application component ID of a DB connection."); + } + if ($this->enableCaching) { + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException("DbMessageSource::cache must be either a cache object or the application component ID of the cache object."); + } + } + } - /** - * Loads the message translation for the specified language and category. - * If translation for specific locale code such as `en-US` isn't found it - * tries more generic `en`. - * - * @param string $category the message category - * @param string $language the target language - * @return array the loaded messages. The keys are original messages, and the values - * are translated messages. - */ - protected function loadMessages($category, $language) - { - if ($this->enableCaching) { - $key = [ - __CLASS__, - $category, - $language, - ]; - $messages = $this->cache->get($key); - if ($messages === false) { - $messages = $this->loadMessagesFromDb($category, $language); - $this->cache->set($key, $messages, $this->cachingDuration); - } - return $messages; - } else { - return $this->loadMessagesFromDb($category, $language); - } - } + /** + * Loads the message translation for the specified language and category. + * If translation for specific locale code such as `en-US` isn't found it + * tries more generic `en`. + * + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + if ($this->enableCaching) { + $key = [ + __CLASS__, + $category, + $language, + ]; + $messages = $this->cache->get($key); + if ($messages === false) { + $messages = $this->loadMessagesFromDb($category, $language); + $this->cache->set($key, $messages, $this->cachingDuration); + } - /** - * Loads the messages from database. - * You may override this method to customize the message storage in the database. - * @param string $category the message category. - * @param string $language the target language. - * @return array the messages loaded from database. - */ - protected function loadMessagesFromDb($category, $language) - { - $mainQuery = new Query(); - $mainQuery->select(['t1.message message', 't2.translation translation']) - ->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2']) - ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :language') - ->params([':category' => $category, ':language' => $language]); + return $messages; + } else { + return $this->loadMessagesFromDb($category, $language); + } + } - $fallbackLanguage = substr($language, 0, 2); - if ($fallbackLanguage != $language) { - $fallbackQuery = new Query(); - $fallbackQuery->select(['t1.message message', 't2.translation translation']) - ->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2']) - ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :fallbackLanguage') - ->andWhere('t2.id NOT IN (SELECT id FROM '.$this->messageTable.' WHERE language = :language)') - ->params([':category' => $category, ':language' => $language, ':fallbackLanguage' => $fallbackLanguage]); + /** + * Loads the messages from database. + * You may override this method to customize the message storage in the database. + * @param string $category the message category. + * @param string $language the target language. + * @return array the messages loaded from database. + */ + protected function loadMessagesFromDb($category, $language) + { + $mainQuery = new Query(); + $mainQuery->select(['t1.message message', 't2.translation translation']) + ->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2']) + ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :language') + ->params([':category' => $category, ':language' => $language]); - $mainQuery->union($fallbackQuery, true); - } + $fallbackLanguage = substr($language, 0, 2); + if ($fallbackLanguage != $language) { + $fallbackQuery = new Query(); + $fallbackQuery->select(['t1.message message', 't2.translation translation']) + ->from([$this->sourceMessageTable . ' t1', $this->messageTable . ' t2']) + ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :fallbackLanguage') + ->andWhere('t2.id NOT IN (SELECT id FROM '.$this->messageTable.' WHERE language = :language)') + ->params([':category' => $category, ':language' => $language, ':fallbackLanguage' => $fallbackLanguage]); - $messages = $mainQuery->createCommand($this->db)->queryAll(); - return ArrayHelper::map($messages, 'message', 'translation'); - } + $mainQuery->union($fallbackQuery, true); + } + + $messages = $mainQuery->createCommand($this->db)->queryAll(); + + return ArrayHelper::map($messages, 'message', 'translation'); + } } diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 4619facfeff..a2f14e4afa3 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -33,286 +33,293 @@ */ class Formatter extends \yii\base\Formatter { - /** - * @var string the locale ID that is used to localize the date and number formatting. - * If not set, [[\yii\base\Application::language]] will be used. - */ - public $locale; - /** - * @var string the default format string to be used to format a date. - * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. - * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). - */ - public $dateFormat = 'short'; - /** - * @var string the default format string to be used to format a time. - * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. - * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). - */ - public $timeFormat = 'short'; - /** - * @var string the default format string to be used to format a date and time. - * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. - * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). - */ - public $datetimeFormat = 'short'; - /** - * @var array the options to be set for the NumberFormatter objects. Please refer to - * [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute) - * for the possible options. This property is used by [[createNumberFormatter]] when - * creating a new number formatter to format decimals, currencies, etc. - */ - public $numberFormatOptions = []; - /** - * @var string the character displayed as the decimal point when formatting a number. - * If not set, the decimal separator corresponding to [[locale]] will be used. - */ - public $decimalSeparator; - /** - * @var string the character displayed as the thousands separator character when formatting a number. - * If not set, the thousand separator corresponding to [[locale]] will be used. - */ - public $thousandSeparator; + /** + * @var string the locale ID that is used to localize the date and number formatting. + * If not set, [[\yii\base\Application::language]] will be used. + */ + public $locale; + /** + * @var string the default format string to be used to format a date. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + */ + public $dateFormat = 'short'; + /** + * @var string the default format string to be used to format a time. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + */ + public $timeFormat = 'short'; + /** + * @var string the default format string to be used to format a date and time. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + */ + public $datetimeFormat = 'short'; + /** + * @var array the options to be set for the NumberFormatter objects. Please refer to + * [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute) + * for the possible options. This property is used by [[createNumberFormatter]] when + * creating a new number formatter to format decimals, currencies, etc. + */ + public $numberFormatOptions = []; + /** + * @var string the character displayed as the decimal point when formatting a number. + * If not set, the decimal separator corresponding to [[locale]] will be used. + */ + public $decimalSeparator; + /** + * @var string the character displayed as the thousands separator character when formatting a number. + * If not set, the thousand separator corresponding to [[locale]] will be used. + */ + public $thousandSeparator; + /** + * Initializes the component. + * This method will check if the "intl" PHP extension is installed and set the + * default value of [[locale]]. + * @throws InvalidConfigException if the "intl" PHP extension is not installed. + */ + public function init() + { + if (!extension_loaded('intl')) { + throw new InvalidConfigException('The "intl" PHP extension is not installed. It is required to format data values in localized formats.'); + } + if ($this->locale === null) { + $this->locale = Yii::$app->language; + } + if ($this->decimalSeparator === null || $this->thousandSeparator === null) { + $formatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL); + if ($this->decimalSeparator === null) { + $this->decimalSeparator = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + } + if ($this->thousandSeparator === null) { + $this->thousandSeparator = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL); + } + } - /** - * Initializes the component. - * This method will check if the "intl" PHP extension is installed and set the - * default value of [[locale]]. - * @throws InvalidConfigException if the "intl" PHP extension is not installed. - */ - public function init() - { - if (!extension_loaded('intl')) { - throw new InvalidConfigException('The "intl" PHP extension is not installed. It is required to format data values in localized formats.'); - } - if ($this->locale === null) { - $this->locale = Yii::$app->language; - } - if ($this->decimalSeparator === null || $this->thousandSeparator === null) { - $formatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL); - if ($this->decimalSeparator === null) { - $this->decimalSeparator = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); - } - if ($this->thousandSeparator === null) { - $this->thousandSeparator = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL); - } - } + parent::init(); + } - parent::init(); - } + private $_dateFormats = [ + 'short' => IntlDateFormatter::SHORT, + 'medium' => IntlDateFormatter::MEDIUM, + 'long' => IntlDateFormatter::LONG, + 'full' => IntlDateFormatter::FULL, + ]; - private $_dateFormats = [ - 'short' => IntlDateFormatter::SHORT, - 'medium' => IntlDateFormatter::MEDIUM, - 'long' => IntlDateFormatter::LONG, - 'full' => IntlDateFormatter::FULL, - ]; + /** + * Formats the value as a date. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * + * @return string the formatted result + * @throws InvalidConfigException when formatting fails due to invalid parameters. + * @see dateFormat + */ + public function asDate($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->dateFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $this->timeZone); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone); + if ($formatter !== null) { + $formatter->setPattern($format); + } + } + if ($formatter === null) { + throw new InvalidConfigException(intl_get_error_message()); + } - /** - * Formats the value as a date. - * @param integer|string|DateTime $value the value to be formatted. The following - * types of value are supported: - * - * - an integer representing a UNIX timestamp - * - a string that can be parsed into a UNIX timestamp via `strtotime()` - * - a PHP DateTime object - * - * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. - * - * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. - * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). - * - * @return string the formatted result - * @throws InvalidConfigException when formatting fails due to invalid parameters. - * @see dateFormat - */ - public function asDate($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - $value = $this->normalizeDatetimeValue($value); - if ($format === null) { - $format = $this->dateFormat; - } - if (isset($this->_dateFormats[$format])) { - $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $this->timeZone); - } else { - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone); - if ($formatter !== null) { - $formatter->setPattern($format); - } - } - if ($formatter === null) { - throw new InvalidConfigException(intl_get_error_message()); - } - return $formatter->format($value); - } + return $formatter->format($value); + } - /** - * Formats the value as a time. - * @param integer|string|DateTime $value the value to be formatted. The following - * types of value are supported: - * - * - an integer representing a UNIX timestamp - * - a string that can be parsed into a UNIX timestamp via `strtotime()` - * - a PHP DateTime object - * - * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. - * - * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. - * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). - * - * @return string the formatted result - * @throws InvalidConfigException when formatting fails due to invalid parameters. - * @see timeFormat - */ - public function asTime($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - $value = $this->normalizeDatetimeValue($value); - if ($format === null) { - $format = $this->timeFormat; - } - if (isset($this->_dateFormats[$format])) { - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $this->timeZone); - } else { - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone); - if ($formatter !== null) { - $formatter->setPattern($format); - } - } - if ($formatter === null) { - throw new InvalidConfigException(intl_get_error_message()); - } - return $formatter->format($value); - } + /** + * Formats the value as a time. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * + * @return string the formatted result + * @throws InvalidConfigException when formatting fails due to invalid parameters. + * @see timeFormat + */ + public function asTime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->timeFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $this->timeZone); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone); + if ($formatter !== null) { + $formatter->setPattern($format); + } + } + if ($formatter === null) { + throw new InvalidConfigException(intl_get_error_message()); + } - /** - * Formats the value as a datetime. - * @param integer|string|DateTime $value the value to be formatted. The following - * types of value are supported: - * - * - an integer representing a UNIX timestamp - * - a string that can be parsed into a UNIX timestamp via `strtotime()` - * - a PHP DateTime object - * - * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. - * - * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. - * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). - * - * @return string the formatted result - * @throws InvalidConfigException when formatting fails due to invalid parameters. - * @see datetimeFormat - */ - public function asDatetime($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - $value = $this->normalizeDatetimeValue($value); - if ($format === null) { - $format = $this->datetimeFormat; - } - if (isset($this->_dateFormats[$format])) { - $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $this->timeZone); - } else { - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone); - if ($formatter !== null) { - $formatter->setPattern($format); - } - } - if ($formatter === null) { - throw new InvalidConfigException(intl_get_error_message()); - } - return $formatter->format($value); - } + return $formatter->format($value); + } - /** - * Formats the value as a decimal number. - * @param mixed $value the value to be formatted - * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) - * for details on how to specify a format. - * @return string the formatted result. - */ - public function asDecimal($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - return $this->createNumberFormatter(NumberFormatter::DECIMAL, $format)->format($value); - } + /** + * Formats the value as a datetime. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * + * @return string the formatted result + * @throws InvalidConfigException when formatting fails due to invalid parameters. + * @see datetimeFormat + */ + public function asDatetime($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->datetimeFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $this->timeZone); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone); + if ($formatter !== null) { + $formatter->setPattern($format); + } + } + if ($formatter === null) { + throw new InvalidConfigException(intl_get_error_message()); + } - /** - * Formats the value as a currency number. - * @param mixed $value the value to be formatted - * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use. - * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) - * for details on how to specify a format. - * @return string the formatted result. - */ - public function asCurrency($value, $currency = 'USD', $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - return $this->createNumberFormatter(NumberFormatter::CURRENCY, $format)->formatCurrency($value, $currency); - } + return $formatter->format($value); + } - /** - * Formats the value as a percent number. - * @param mixed $value the value to be formatted - * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) - * for details on how to specify a format. - * @return string the formatted result. - */ - public function asPercent($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - return $this->createNumberFormatter(NumberFormatter::PERCENT, $format)->format($value); - } + /** + * Formats the value as a decimal number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asDecimal($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } - /** - * Formats the value as a scientific number. - * @param mixed $value the value to be formatted - * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) - * for details on how to specify a format. - * @return string the formatted result. - */ - public function asScientific($value, $format = null) - { - if ($value === null) { - return $this->nullDisplay; - } - return $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $format)->format($value); - } + return $this->createNumberFormatter(NumberFormatter::DECIMAL, $format)->format($value); + } - /** - * Creates a number formatter based on the given type and format. - * @param integer $type the type of the number formatter - * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) - * @return NumberFormatter the created formatter instance - */ - protected function createNumberFormatter($type, $format) - { - $formatter = new NumberFormatter($this->locale, $type); - if ($format !== null) { - $formatter->setPattern($format); - } - if (!empty($this->numberFormatOptions)) { - foreach ($this->numberFormatOptions as $name => $attribute) { - $formatter->setAttribute($name, $attribute); - } - } - return $formatter; - } + /** + * Formats the value as a currency number. + * @param mixed $value the value to be formatted + * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use. + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asCurrency($value, $currency = 'USD', $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + + return $this->createNumberFormatter(NumberFormatter::CURRENCY, $format)->formatCurrency($value, $currency); + } + + /** + * Formats the value as a percent number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asPercent($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + + return $this->createNumberFormatter(NumberFormatter::PERCENT, $format)->format($value); + } + + /** + * Formats the value as a scientific number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asScientific($value, $format = null) + { + if ($value === null) { + return $this->nullDisplay; + } + + return $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $format)->format($value); + } + + /** + * Creates a number formatter based on the given type and format. + * @param integer $type the type of the number formatter + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * @return NumberFormatter the created formatter instance + */ + protected function createNumberFormatter($type, $format) + { + $formatter = new NumberFormatter($this->locale, $type); + if ($format !== null) { + $formatter->setPattern($format); + } + if (!empty($this->numberFormatOptions)) { + foreach ($this->numberFormatOptions as $name => $attribute) { + $formatter->setAttribute($name, $attribute); + } + } + + return $formatter; + } } diff --git a/framework/i18n/GettextFile.php b/framework/i18n/GettextFile.php index 03eecca2767..03fea498feb 100644 --- a/framework/i18n/GettextFile.php +++ b/framework/i18n/GettextFile.php @@ -17,21 +17,21 @@ */ abstract class GettextFile extends Component { - /** - * Loads messages from a file. - * @param string $filePath file path - * @param string $context message context - * @return array message translations. Array keys are source messages and array values are translated messages: - * source message => translated message. - */ - abstract public function load($filePath, $context); + /** + * Loads messages from a file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + abstract public function load($filePath, $context); - /** - * Saves messages to a file. - * @param string $filePath file path - * @param array $messages message translations. Array keys are source messages and array values are - * translated messages: source message => translated message. Note if the message has a context, - * the message ID must be prefixed with the context with chr(4) as the separator. - */ - abstract public function save($filePath, $messages); + /** + * Saves messages to a file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + abstract public function save($filePath, $messages); } diff --git a/framework/i18n/GettextMessageSource.php b/framework/i18n/GettextMessageSource.php index fc6d87fa670..813e1fbe55e 100644 --- a/framework/i18n/GettextMessageSource.php +++ b/framework/i18n/GettextMessageSource.php @@ -28,104 +28,107 @@ */ class GettextMessageSource extends MessageSource { - const MO_FILE_EXT = '.mo'; - const PO_FILE_EXT = '.po'; + const MO_FILE_EXT = '.mo'; + const PO_FILE_EXT = '.po'; - /** - * @var string - */ - public $basePath = '@app/messages'; - /** - * @var string - */ - public $catalog = 'messages'; - /** - * @var boolean - */ - public $useMoFile = true; - /** - * @var boolean - */ - public $useBigEndian = false; + /** + * @var string + */ + public $basePath = '@app/messages'; + /** + * @var string + */ + public $catalog = 'messages'; + /** + * @var boolean + */ + public $useMoFile = true; + /** + * @var boolean + */ + public $useBigEndian = false; - /** - * Loads the message translation for the specified language and category. - * If translation for specific locale code such as `en-US` isn't found it - * tries more generic `en`. - * - * @param string $category the message category - * @param string $language the target language - * @return array the loaded messages. The keys are original messages, and the values - * are translated messages. - */ - protected function loadMessages($category, $language) - { - $messageFile = $this->getMessageFilePath($language); - $messages = $this->loadMessagesFromFile($messageFile, $category); + /** + * Loads the message translation for the specified language and category. + * If translation for specific locale code such as `en-US` isn't found it + * tries more generic `en`. + * + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + $messageFile = $this->getMessageFilePath($language); + $messages = $this->loadMessagesFromFile($messageFile, $category); - $fallbackLanguage = substr($language, 0, 2); - if ($fallbackLanguage != $language) { - $fallbackMessageFile = $this->getMessageFilePath($fallbackLanguage); - $fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile, $category); + $fallbackLanguage = substr($language, 0, 2); + if ($fallbackLanguage != $language) { + $fallbackMessageFile = $this->getMessageFilePath($fallbackLanguage); + $fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile, $category); - if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) { - Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); - } elseif (empty($messages)) { - return $fallbackMessages; - } elseif (!empty($fallbackMessages)) { - foreach ($fallbackMessages as $key => $value) { - if (!empty($value) && empty($messages[$key])) { - $messages[$key] = $fallbackMessages[$key]; - } - } - } - } else { - if ($messages === null) { - Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); - } - } - return (array)$messages; - } + if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) { + Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); + } elseif (empty($messages)) { + return $fallbackMessages; + } elseif (!empty($fallbackMessages)) { + foreach ($fallbackMessages as $key => $value) { + if (!empty($value) && empty($messages[$key])) { + $messages[$key] = $fallbackMessages[$key]; + } + } + } + } else { + if ($messages === null) { + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); + } + } - /** - * Returns message file path for the specified language and category. - * - * @param string $language the target language - * @return string path to message file - */ - protected function getMessageFilePath($language) - { - $messageFile = Yii::getAlias($this->basePath) . '/' . $language . '/' . $this->catalog; - if ($this->useMoFile) { - $messageFile .= static::MO_FILE_EXT; - } else { - $messageFile .= static::PO_FILE_EXT; - } - return $messageFile; - } + return (array) $messages; + } - /** - * Loads the message translation for the specified language and category or returns null if file doesn't exist. - * - * @param string $messageFile path to message file - * @param string $category the message category - * @return array|null array of messages or null if file not found - */ - protected function loadMessagesFromFile($messageFile, $category) - { - if (is_file($messageFile)) { - if ($this->useMoFile) { - $gettextFile = new GettextMoFile(['useBigEndian' => $this->useBigEndian]); - } else { - $gettextFile = new GettextPoFile(); - } - $messages = $gettextFile->load($messageFile, $category); - if (!is_array($messages)) { - $messages = []; - } - return $messages; - } else { - return null; - } - } + /** + * Returns message file path for the specified language and category. + * + * @param string $language the target language + * @return string path to message file + */ + protected function getMessageFilePath($language) + { + $messageFile = Yii::getAlias($this->basePath) . '/' . $language . '/' . $this->catalog; + if ($this->useMoFile) { + $messageFile .= static::MO_FILE_EXT; + } else { + $messageFile .= static::PO_FILE_EXT; + } + + return $messageFile; + } + + /** + * Loads the message translation for the specified language and category or returns null if file doesn't exist. + * + * @param string $messageFile path to message file + * @param string $category the message category + * @return array|null array of messages or null if file not found + */ + protected function loadMessagesFromFile($messageFile, $category) + { + if (is_file($messageFile)) { + if ($this->useMoFile) { + $gettextFile = new GettextMoFile(['useBigEndian' => $this->useBigEndian]); + } else { + $gettextFile = new GettextPoFile(); + } + $messages = $gettextFile->load($messageFile, $category); + if (!is_array($messages)) { + $messages = []; + } + + return $messages; + } else { + return null; + } + } } diff --git a/framework/i18n/GettextMoFile.php b/framework/i18n/GettextMoFile.php index b4a016de520..15806af2342 100644 --- a/framework/i18n/GettextMoFile.php +++ b/framework/i18n/GettextMoFile.php @@ -43,229 +43,232 @@ */ class GettextMoFile extends GettextFile { - /** - * @var boolean whether to use big-endian when reading and writing an integer. - */ - public $useBigEndian = false; - - /** - * Loads messages from an MO file. - * @param string $filePath file path - * @param string $context message context - * @return array message translations. Array keys are source messages and array values are translated messages: - * source message => translated message. - * @throws Exception if unable to read the MO file - */ - public function load($filePath, $context) - { - if (false === ($fileHandle = @fopen($filePath, 'rb'))) { - throw new Exception('Unable to read file "' . $filePath . '".'); - } - if (false === @flock($fileHandle, LOCK_SH)) { - throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); - } - - // magic - $array = unpack('c', $this->readBytes($fileHandle, 4)); - $magic = current($array); - if ($magic == -34) { - $this->useBigEndian = false; - } elseif ($magic == -107) { - $this->useBigEndian = true; - } else { - throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').'); - } - - // revision - $revision = $this->readInteger($fileHandle); - if ($revision != 0) { - throw new Exception('Invalid MO file revision: ' . $revision . '.'); - } - - $count = $this->readInteger($fileHandle); - $sourceOffset = $this->readInteger($fileHandle); - $targetOffset = $this->readInteger($fileHandle); - - $sourceLengths = []; - $sourceOffsets = []; - fseek($fileHandle, $sourceOffset); - for ($i = 0; $i < $count; ++$i) { - $sourceLengths[] = $this->readInteger($fileHandle); - $sourceOffsets[] = $this->readInteger($fileHandle); - } - - $targetLengths = []; - $targetOffsets = []; - fseek($fileHandle, $targetOffset); - for ($i = 0; $i < $count; ++$i) { - $targetLengths[] = $this->readInteger($fileHandle); - $targetOffsets[] = $this->readInteger($fileHandle); - } - - $messages = []; - for ($i = 0; $i < $count; ++$i) { - $id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]); - $separatorPosition = strpos($id, chr(4)); - - if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) || - (!$context && $separatorPosition === false)) { - if ($separatorPosition !== false) { - $id = substr($id, $separatorPosition+1); - } - - $message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]); - $messages[$id] = $message; - } - } - - @flock($fileHandle, LOCK_UN); - @fclose($fileHandle); - return $messages; - } - - /** - * Saves messages to an MO file. - * @param string $filePath file path - * @param array $messages message translations. Array keys are source messages and array values are - * translated messages: source message => translated message. Note if the message has a context, - * the message ID must be prefixed with the context with chr(4) as the separator. - * @throws Exception if unable to save the MO file - */ - public function save($filePath, $messages) - { - if (false === ($fileHandle = @fopen($filePath, 'wb'))) { - throw new Exception('Unable to write file "' . $filePath . '".'); - } - if (false === @flock($fileHandle, LOCK_EX)) { - throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); - } - - // magic - if ($this->useBigEndian) { - $this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107 - } else { - $this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34 - } - - // revision - $this->writeInteger($fileHandle, 0); - - // message count - $messageCount = count($messages); - $this->writeInteger($fileHandle, $messageCount); - - // offset of source message table - $offset = 28; - $this->writeInteger($fileHandle, $offset); - $offset += $messageCount * 8; - $this->writeInteger($fileHandle, $offset); - - // hashtable size, omitted - $this->writeInteger($fileHandle, 0); - $offset += $messageCount * 8; - $this->writeInteger($fileHandle, $offset); - - // length and offsets for source messages - foreach (array_keys($messages) as $id) { - $length = strlen($id); - $this->writeInteger($fileHandle, $length); - $this->writeInteger($fileHandle, $offset); - $offset += $length + 1; - } - - // length and offsets for target messages - foreach ($messages as $message) { - $length = strlen($message); - $this->writeInteger($fileHandle, $length); - $this->writeInteger($fileHandle, $offset); - $offset += $length + 1; - } - - // source messages - foreach (array_keys($messages) as $id) { - $this->writeString($fileHandle, $id); - } - - // target messages - foreach ($messages as $message) { - $this->writeString($fileHandle, $message); - } - - @flock($fileHandle, LOCK_UN); - @fclose($fileHandle); - } - - /** - * Reads one or several bytes. - * @param resource $fileHandle to read from - * @param integer $byteCount to be read - * @return string bytes - */ - protected function readBytes($fileHandle, $byteCount = 1) - { - if ($byteCount > 0) { - return fread($fileHandle, $byteCount); - } else { - return null; - } - } - - /** - * Write bytes. - * @param resource $fileHandle to write to - * @param string $bytes to be written - * @return integer how many bytes are written - */ - protected function writeBytes($fileHandle, $bytes) - { - return fwrite($fileHandle, $bytes); - } - - /** - * Reads a 4-byte integer. - * @param resource $fileHandle to read from - * @return integer the result - */ - protected function readInteger($fileHandle) - { - $array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4)); - return current($array); - } - - /** - * Writes a 4-byte integer. - * @param resource $fileHandle to write to - * @param integer $integer to be written - * @return integer how many bytes are written - */ - protected function writeInteger($fileHandle, $integer) - { - return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int)$integer)); - } - - /** - * Reads a string. - * @param resource $fileHandle file handle - * @param integer $length of the string - * @param integer $offset of the string in the file. If null, it reads from the current position. - * @return string the result - */ - protected function readString($fileHandle, $length, $offset = null) - { - if ($offset !== null) { - fseek($fileHandle, $offset); - } - return $this->readBytes($fileHandle, $length); - } - - /** - * Writes a string. - * @param resource $fileHandle to write to - * @param string $string to be written - * @return integer how many bytes are written - */ - protected function writeString($fileHandle, $string) - { - return $this->writeBytes($fileHandle, $string. "\0"); - } + /** + * @var boolean whether to use big-endian when reading and writing an integer. + */ + public $useBigEndian = false; + + /** + * Loads messages from an MO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + * @throws Exception if unable to read the MO file + */ + public function load($filePath, $context) + { + if (false === ($fileHandle = @fopen($filePath, 'rb'))) { + throw new Exception('Unable to read file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_SH)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + $array = unpack('c', $this->readBytes($fileHandle, 4)); + $magic = current($array); + if ($magic == -34) { + $this->useBigEndian = false; + } elseif ($magic == -107) { + $this->useBigEndian = true; + } else { + throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').'); + } + + // revision + $revision = $this->readInteger($fileHandle); + if ($revision != 0) { + throw new Exception('Invalid MO file revision: ' . $revision . '.'); + } + + $count = $this->readInteger($fileHandle); + $sourceOffset = $this->readInteger($fileHandle); + $targetOffset = $this->readInteger($fileHandle); + + $sourceLengths = []; + $sourceOffsets = []; + fseek($fileHandle, $sourceOffset); + for ($i = 0; $i < $count; ++$i) { + $sourceLengths[] = $this->readInteger($fileHandle); + $sourceOffsets[] = $this->readInteger($fileHandle); + } + + $targetLengths = []; + $targetOffsets = []; + fseek($fileHandle, $targetOffset); + for ($i = 0; $i < $count; ++$i) { + $targetLengths[] = $this->readInteger($fileHandle); + $targetOffsets[] = $this->readInteger($fileHandle); + } + + $messages = []; + for ($i = 0; $i < $count; ++$i) { + $id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]); + $separatorPosition = strpos($id, chr(4)); + + if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) || + (!$context && $separatorPosition === false)) { + if ($separatorPosition !== false) { + $id = substr($id, $separatorPosition+1); + } + + $message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]); + $messages[$id] = $message; + } + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + + return $messages; + } + + /** + * Saves messages to an MO file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + * @throws Exception if unable to save the MO file + */ + public function save($filePath, $messages) + { + if (false === ($fileHandle = @fopen($filePath, 'wb'))) { + throw new Exception('Unable to write file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_EX)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + if ($this->useBigEndian) { + $this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107 + } else { + $this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34 + } + + // revision + $this->writeInteger($fileHandle, 0); + + // message count + $messageCount = count($messages); + $this->writeInteger($fileHandle, $messageCount); + + // offset of source message table + $offset = 28; + $this->writeInteger($fileHandle, $offset); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // hashtable size, omitted + $this->writeInteger($fileHandle, 0); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // length and offsets for source messages + foreach (array_keys($messages) as $id) { + $length = strlen($id); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // length and offsets for target messages + foreach ($messages as $message) { + $length = strlen($message); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // source messages + foreach (array_keys($messages) as $id) { + $this->writeString($fileHandle, $id); + } + + // target messages + foreach ($messages as $message) { + $this->writeString($fileHandle, $message); + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + } + + /** + * Reads one or several bytes. + * @param resource $fileHandle to read from + * @param integer $byteCount to be read + * @return string bytes + */ + protected function readBytes($fileHandle, $byteCount = 1) + { + if ($byteCount > 0) { + return fread($fileHandle, $byteCount); + } else { + return null; + } + } + + /** + * Write bytes. + * @param resource $fileHandle to write to + * @param string $bytes to be written + * @return integer how many bytes are written + */ + protected function writeBytes($fileHandle, $bytes) + { + return fwrite($fileHandle, $bytes); + } + + /** + * Reads a 4-byte integer. + * @param resource $fileHandle to read from + * @return integer the result + */ + protected function readInteger($fileHandle) + { + $array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4)); + + return current($array); + } + + /** + * Writes a 4-byte integer. + * @param resource $fileHandle to write to + * @param integer $integer to be written + * @return integer how many bytes are written + */ + protected function writeInteger($fileHandle, $integer) + { + return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int) $integer)); + } + + /** + * Reads a string. + * @param resource $fileHandle file handle + * @param integer $length of the string + * @param integer $offset of the string in the file. If null, it reads from the current position. + * @return string the result + */ + protected function readString($fileHandle, $length, $offset = null) + { + if ($offset !== null) { + fseek($fileHandle, $offset); + } + + return $this->readBytes($fileHandle, $length); + } + + /** + * Writes a string. + * @param resource $fileHandle to write to + * @param string $string to be written + * @return integer how many bytes are written + */ + protected function writeString($fileHandle, $string) + { + return $this->writeBytes($fileHandle, $string. "\0"); + } } diff --git a/framework/i18n/GettextPoFile.php b/framework/i18n/GettextPoFile.php index 8fb618cb755..95998b8968f 100644 --- a/framework/i18n/GettextPoFile.php +++ b/framework/i18n/GettextPoFile.php @@ -17,81 +17,83 @@ */ class GettextPoFile extends GettextFile { - /** - * Loads messages from a PO file. - * @param string $filePath file path - * @param string $context message context - * @return array message translations. Array keys are source messages and array values are translated messages: - * source message => translated message. - */ - public function load($filePath, $context) - { - $pattern = '/(msgctxt\s+"(.*?(? translated message. + */ + public function load($filePath, $context) + { + $pattern = '/(msgctxt\s+"(.*?(?decode($matches[3][$i]); - $message = $this->decode($matches[4][$i]); - $messages[$id] = $message; - } - } - return $messages; - } + $messages = []; + for ($i = 0; $i < $matchCount; ++$i) { + if ($matches[2][$i] == $context) { + $id = $this->decode($matches[3][$i]); + $message = $this->decode($matches[4][$i]); + $messages[$id] = $message; + } + } - /** - * Saves messages to a PO file. - * @param string $filePath file path - * @param array $messages message translations. Array keys are source messages and array values are - * translated messages: source message => translated message. Note if the message has a context, - * the message ID must be prefixed with the context with chr(4) as the separator. - */ - public function save($filePath, $messages) - { - $content = ''; - foreach ($messages as $id => $message) { - $separatorPosition = strpos($id, chr(4)); - if ($separatorPosition !== false) { - $content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n"; - $id = substr($id, $separatorPosition + 1); - } - $content .= 'msgid "' . $this->encode($id) . "\"\n"; - $content .= 'msgstr "' . $this->encode($message) . "\"\n\n"; - } - file_put_contents($filePath, $content); - } + return $messages; + } - /** - * Encodes special characters in a message. - * @param string $string message to be encoded - * @return string the encoded message - */ - protected function encode($string) - { - return str_replace( - ['"', "\n", "\t", "\r"], - ['\\"', '\\n', '\\t', '\\r'], - $string - ); - } + /** + * Saves messages to a PO file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + public function save($filePath, $messages) + { + $content = ''; + foreach ($messages as $id => $message) { + $separatorPosition = strpos($id, chr(4)); + if ($separatorPosition !== false) { + $content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n"; + $id = substr($id, $separatorPosition + 1); + } + $content .= 'msgid "' . $this->encode($id) . "\"\n"; + $content .= 'msgstr "' . $this->encode($message) . "\"\n\n"; + } + file_put_contents($filePath, $content); + } - /** - * Decodes special characters in a message. - * @param string $string message to be decoded - * @return string the decoded message - */ - protected function decode($string) - { - $string = preg_replace( - ['/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'], - ['', "\n", "\r", "\t", '"'], - $string - ); - return substr(rtrim($string), 1, -1); - } + /** + * Encodes special characters in a message. + * @param string $string message to be encoded + * @return string the encoded message + */ + protected function encode($string) + { + return str_replace( + ['"', "\n", "\t", "\r"], + ['\\"', '\\n', '\\t', '\\r'], + $string + ); + } + + /** + * Decodes special characters in a message. + * @param string $string message to be decoded + * @return string the decoded message + */ + protected function decode($string) + { + $string = preg_replace( + ['/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'], + ['', "\n", "\r", "\t", '"'], + $string + ); + + return substr(rtrim($string), 1, -1); + } } diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index b53366eece3..4a9c05e1721 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -26,173 +26,176 @@ */ class I18N extends Component { - /** - * @var array list of [[MessageSource]] configurations or objects. The array keys are message - * category patterns, and the array values are the corresponding [[MessageSource]] objects or the configurations - * for creating the [[MessageSource]] objects. - * - * The message category patterns can contain the wildcard '*' at the end to match multiple categories with the same prefix. - * For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. - * - * The '*' category pattern will match all categories that do not match any other category patterns. - * - * This property may be modified on the fly by extensions who want to have their own message sources - * registered under their own namespaces. - * - * The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core - * framework code, while the latter refers to the default message category for custom application code. - * By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are - * stored under "@yii/messages" and "@app/messages", respectively. - * - * You may override the configuration of both categories. - */ - public $translations; - - /** - * Initializes the component by configuring the default message categories. - */ - public function init() - { - parent::init(); - if (!isset($this->translations['yii'])) { - $this->translations['yii'] = [ - 'class' => 'yii\i18n\PhpMessageSource', - 'sourceLanguage' => 'en', - 'basePath' => '@yii/messages', - ]; - } - if (!isset($this->translations['app'])) { - $this->translations['app'] = [ - 'class' => 'yii\i18n\PhpMessageSource', - 'sourceLanguage' => Yii::$app->sourceLanguage, - 'basePath' => '@app/messages', - ]; - } - } - - /** - * Translates a message to the specified language. - * - * After translation the message will be formatted using [[MessageFormatter]] if it contains - * ICU message format and `$params` are not empty. - * - * @param string $category the message category. - * @param string $message the message to be translated. - * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. - * @param string $language the language code (e.g. `en-US`, `en`). - * @return string the translated and formatted message. - */ - public function translate($category, $message, $params, $language) - { - $messageSource = $this->getMessageSource($category); - $translation = $messageSource->translate($category, $message, $language); - if ($translation === false) { - return $this->format($message, $params, $messageSource->sourceLanguage); - } else { - return $this->format($translation, $params, $language); - } - } - - /** - * Formats a message using [[MessageFormatter]]. - * - * @param string $message the message to be formatted. - * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. - * @param string $language the language code (e.g. `en-US`, `en`). - * @return string the formatted message. - */ - public function format($message, $params, $language) - { - $params = (array)$params; - if ($params === []) { - return $message; - } - - if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) { - $formatter = $this->getMessageFormatter(); - $result = $formatter->format($message, $params, $language); - if ($result === false) { - $errorMessage = $formatter->getErrorMessage(); - Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message."); - return $message; - } else { - return $result; - } - } - - $p = []; - foreach ($params as $name => $value) { - $p['{' . $name . '}'] = $value; - } - return strtr($message, $p); - } - - /** - * @var string|array|MessageFormatter - */ - private $_messageFormatter; - - /** - * Returns the message formatter instance. - * @return MessageFormatter the message formatter to be used to format message via ICU message format. - */ - public function getMessageFormatter() - { - if ($this->_messageFormatter === null) { - $this->_messageFormatter = new MessageFormatter(); - } elseif (is_array($this->_messageFormatter) || is_string($this->_messageFormatter)) { - $this->_messageFormatter = Yii::createObject($this->_messageFormatter); - } - return $this->_messageFormatter; - } - - /** - * @param string|array|MessageFormatter $value the message formatter to be used to format message via ICU message format. - * Can be given as array or string configuration that will be given to [[Yii::createObject]] to create an instance - * or a [[MessageFormatter]] instance. - */ - public function setMessageFormatter($value) - { - $this->_messageFormatter = $value; - } - - /** - * Returns the message source for the given category. - * @param string $category the category name. - * @return MessageSource the message source for the given category. - * @throws InvalidConfigException if there is no message source available for the specified category. - */ - public function getMessageSource($category) - { - if (isset($this->translations[$category])) { - $source = $this->translations[$category]; - if ($source instanceof MessageSource) { - return $source; - } else { - return $this->translations[$category] = Yii::createObject($source); - } - } else { - // try wildcard matching - foreach ($this->translations as $pattern => $source) { - if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) { - if ($source instanceof MessageSource) { - return $source; - } else { - return $this->translations[$category] = $this->translations[$pattern] = Yii::createObject($source); - } - } - } - // match '*' in the last - if (isset($this->translations['*'])) { - $source = $this->translations['*']; - if ($source instanceof MessageSource) { - return $source; - } else { - return $this->translations[$category] = $this->translations['*'] = Yii::createObject($source); - } - } - } - - throw new InvalidConfigException("Unable to locate message source for category '$category'."); - } + /** + * @var array list of [[MessageSource]] configurations or objects. The array keys are message + * category patterns, and the array values are the corresponding [[MessageSource]] objects or the configurations + * for creating the [[MessageSource]] objects. + * + * The message category patterns can contain the wildcard '*' at the end to match multiple categories with the same prefix. + * For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + * + * The '*' category pattern will match all categories that do not match any other category patterns. + * + * This property may be modified on the fly by extensions who want to have their own message sources + * registered under their own namespaces. + * + * The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core + * framework code, while the latter refers to the default message category for custom application code. + * By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are + * stored under "@yii/messages" and "@app/messages", respectively. + * + * You may override the configuration of both categories. + */ + public $translations; + + /** + * Initializes the component by configuring the default message categories. + */ + public function init() + { + parent::init(); + if (!isset($this->translations['yii'])) { + $this->translations['yii'] = [ + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en', + 'basePath' => '@yii/messages', + ]; + } + if (!isset($this->translations['app'])) { + $this->translations['app'] = [ + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => Yii::$app->sourceLanguage, + 'basePath' => '@app/messages', + ]; + } + } + + /** + * Translates a message to the specified language. + * + * After translation the message will be formatted using [[MessageFormatter]] if it contains + * ICU message format and `$params` are not empty. + * + * @param string $category the message category. + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en-US`, `en`). + * @return string the translated and formatted message. + */ + public function translate($category, $message, $params, $language) + { + $messageSource = $this->getMessageSource($category); + $translation = $messageSource->translate($category, $message, $language); + if ($translation === false) { + return $this->format($message, $params, $messageSource->sourceLanguage); + } else { + return $this->format($translation, $params, $language); + } + } + + /** + * Formats a message using [[MessageFormatter]]. + * + * @param string $message the message to be formatted. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en-US`, `en`). + * @return string the formatted message. + */ + public function format($message, $params, $language) + { + $params = (array) $params; + if ($params === []) { + return $message; + } + + if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) { + $formatter = $this->getMessageFormatter(); + $result = $formatter->format($message, $params, $language); + if ($result === false) { + $errorMessage = $formatter->getErrorMessage(); + Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message."); + + return $message; + } else { + return $result; + } + } + + $p = []; + foreach ($params as $name => $value) { + $p['{' . $name . '}'] = $value; + } + + return strtr($message, $p); + } + + /** + * @var string|array|MessageFormatter + */ + private $_messageFormatter; + + /** + * Returns the message formatter instance. + * @return MessageFormatter the message formatter to be used to format message via ICU message format. + */ + public function getMessageFormatter() + { + if ($this->_messageFormatter === null) { + $this->_messageFormatter = new MessageFormatter(); + } elseif (is_array($this->_messageFormatter) || is_string($this->_messageFormatter)) { + $this->_messageFormatter = Yii::createObject($this->_messageFormatter); + } + + return $this->_messageFormatter; + } + + /** + * @param string|array|MessageFormatter $value the message formatter to be used to format message via ICU message format. + * Can be given as array or string configuration that will be given to [[Yii::createObject]] to create an instance + * or a [[MessageFormatter]] instance. + */ + public function setMessageFormatter($value) + { + $this->_messageFormatter = $value; + } + + /** + * Returns the message source for the given category. + * @param string $category the category name. + * @return MessageSource the message source for the given category. + * @throws InvalidConfigException if there is no message source available for the specified category. + */ + public function getMessageSource($category) + { + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; + if ($source instanceof MessageSource) { + return $source; + } else { + return $this->translations[$category] = Yii::createObject($source); + } + } else { + // try wildcard matching + foreach ($this->translations as $pattern => $source) { + if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) { + if ($source instanceof MessageSource) { + return $source; + } else { + return $this->translations[$category] = $this->translations[$pattern] = Yii::createObject($source); + } + } + } + // match '*' in the last + if (isset($this->translations['*'])) { + $source = $this->translations['*']; + if ($source instanceof MessageSource) { + return $source; + } else { + return $this->translations[$category] = $this->translations['*'] = Yii::createObject($source); + } + } + } + + throw new InvalidConfigException("Unable to locate message source for category '$category'."); + } } diff --git a/framework/i18n/MessageFormatter.php b/framework/i18n/MessageFormatter.php index 022e8829698..2c7bd70eb2e 100644 --- a/framework/i18n/MessageFormatter.php +++ b/framework/i18n/MessageFormatter.php @@ -44,355 +44,366 @@ */ class MessageFormatter extends Component { - private $_errorCode = 0; - private $_errorMessage = ''; + private $_errorCode = 0; + private $_errorMessage = ''; - /** - * Get the error code from the last operation - * @link http://php.net/manual/en/messageformatter.geterrorcode.php - * @return string Code of the last error. - */ - public function getErrorCode() - { - return $this->_errorCode; - } + /** + * Get the error code from the last operation + * @link http://php.net/manual/en/messageformatter.geterrorcode.php + * @return string Code of the last error. + */ + public function getErrorCode() + { + return $this->_errorCode; + } - /** - * Get the error text from the last operation - * @link http://php.net/manual/en/messageformatter.geterrormessage.php - * @return string Description of the last error. - */ - public function getErrorMessage() - { - return $this->_errorMessage; - } + /** + * Get the error text from the last operation + * @link http://php.net/manual/en/messageformatter.geterrormessage.php + * @return string Description of the last error. + */ + public function getErrorMessage() + { + return $this->_errorMessage; + } - /** - * Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages) - * - * It uses the PHP intl extension's [MessageFormatter](http://www.php.net/manual/en/class.messageformatter.php) - * and works around some issues. - * If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format. - * - * @param string $pattern The pattern string to insert parameters into. - * @param array $params The array of name value pairs to insert into the format string. - * @param string $language The locale to use for formatting locale-dependent parts - * @return string|boolean The formatted pattern string or `FALSE` if an error occurred - */ - public function format($pattern, $params, $language) - { - $this->_errorCode = 0; - $this->_errorMessage = ''; + /** + * Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages) + * + * It uses the PHP intl extension's [MessageFormatter](http://www.php.net/manual/en/class.messageformatter.php) + * and works around some issues. + * If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format. + * + * @param string $pattern The pattern string to insert parameters into. + * @param array $params The array of name value pairs to insert into the format string. + * @param string $language The locale to use for formatting locale-dependent parts + * @return string|boolean The formatted pattern string or `FALSE` if an error occurred + */ + public function format($pattern, $params, $language) + { + $this->_errorCode = 0; + $this->_errorMessage = ''; - if ($params === []) { - return $pattern; - } + if ($params === []) { + return $pattern; + } - if (!class_exists('MessageFormatter', false)) { - return $this->fallbackFormat($pattern, $params, $language); - } + if (!class_exists('MessageFormatter', false)) { + return $this->fallbackFormat($pattern, $params, $language); + } - if (version_compare(PHP_VERSION, '5.5.0', '<') || version_compare(INTL_ICU_VERSION, '4.8', '<')) { - // replace named arguments - $pattern = $this->replaceNamedArguments($pattern, $params, $newParams); - $params = $newParams; - } + if (version_compare(PHP_VERSION, '5.5.0', '<') || version_compare(INTL_ICU_VERSION, '4.8', '<')) { + // replace named arguments + $pattern = $this->replaceNamedArguments($pattern, $params, $newParams); + $params = $newParams; + } - $formatter = new \MessageFormatter($language, $pattern); - if ($formatter === null) { - $this->_errorCode = intl_get_error_code(); - $this->_errorMessage = "Message pattern is invalid: " . intl_get_error_message(); - return false; - } - $result = $formatter->format($params); - if ($result === false) { - $this->_errorCode = $formatter->getErrorCode(); - $this->_errorMessage = $formatter->getErrorMessage(); - return false; - } else { - return $result; - } - } + $formatter = new \MessageFormatter($language, $pattern); + if ($formatter === null) { + $this->_errorCode = intl_get_error_code(); + $this->_errorMessage = "Message pattern is invalid: " . intl_get_error_message(); - /** - * Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern. - * - * It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php) - * and adds support for named arguments. - * Usage of this method requires PHP intl extension to be installed. - * - * @param string $pattern The pattern to use for parsing the message. - * @param string $message The message to parse, conforming to the pattern. - * @param string $language The locale to use for formatting locale-dependent parts - * @return array|boolean An array containing items extracted, or `FALSE` on error. - * @throws \yii\base\NotSupportedException when PHP intl extension is not installed. - */ - public function parse($pattern, $message, $language) - { - $this->_errorCode = 0; - $this->_errorMessage = ''; + return false; + } + $result = $formatter->format($params); + if ($result === false) { + $this->_errorCode = $formatter->getErrorCode(); + $this->_errorMessage = $formatter->getErrorMessage(); - if (!class_exists('MessageFormatter', false)) { - throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); - } + return false; + } else { + return $result; + } + } - // replace named arguments - if (($tokens = self::tokenizePattern($pattern)) === false) { - $this->_errorCode = -1; - $this->_errorMessage = "Message pattern is invalid."; - return false; - } - $map = []; - foreach ($tokens as $i => $token) { - if (is_array($token)) { - $param = trim($token[0]); - if (!isset($map[$param])) { - $map[$param] = count($map); - } - $token[0] = $map[$param]; - $tokens[$i] = '{' . implode(',', $token) . '}'; - } - } - $pattern = implode('', $tokens); - $map = array_flip($map); + /** + * Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern. + * + * It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php) + * and adds support for named arguments. + * Usage of this method requires PHP intl extension to be installed. + * + * @param string $pattern The pattern to use for parsing the message. + * @param string $message The message to parse, conforming to the pattern. + * @param string $language The locale to use for formatting locale-dependent parts + * @return array|boolean An array containing items extracted, or `FALSE` on error. + * @throws \yii\base\NotSupportedException when PHP intl extension is not installed. + */ + public function parse($pattern, $message, $language) + { + $this->_errorCode = 0; + $this->_errorMessage = ''; - $formatter = new \MessageFormatter($language, $pattern); - if ($formatter === null) { - $this->_errorCode = -1; - $this->_errorMessage = "Message pattern is invalid."; - return false; - } - $result = $formatter->parse($message); - if ($result === false) { - $this->_errorCode = $formatter->getErrorCode(); - $this->_errorMessage = $formatter->getErrorMessage(); - return false; - } else { - $values = []; - foreach ($result as $key => $value) { - $values[$map[$key]] = $value; - } - return $values; - } - } + if (!class_exists('MessageFormatter', false)) { + throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); + } - /** - * Replace named placeholders with numeric placeholders and quote unused. - * - * @param string $pattern The pattern string to replace things into. - * @param array $givenParams The array of values to insert into the format string. - * @param array $resultingParams Modified array of parameters. - * @param array $map - * @return string The pattern string with placeholders replaced. - */ - private function replaceNamedArguments($pattern, $givenParams, &$resultingParams, &$map = []) - { - if (($tokens = self::tokenizePattern($pattern)) === false) { - return false; - } - foreach ($tokens as $i => $token) { - if (!is_array($token)) { - continue; - } - $param = trim($token[0]); - if (isset($givenParams[$param])) { - // if param is given, replace it with a number - if (!isset($map[$param])) { - $map[$param] = count($map); - // make sure only used params are passed to format method - $resultingParams[$map[$param]] = $givenParams[$param]; - } - $token[0] = $map[$param]; - $quote = ""; - } else { - // quote unused token - $quote = "'"; - } - $type = isset($token[1]) ? trim($token[1]) : 'none'; - // replace plural and select format recursively - if ($type == 'plural' || $type == 'select') { - if (!isset($token[2])) { - return false; - } - $subtokens = self::tokenizePattern($token[2]); - $c = count($subtokens); - for ($k = 0; $k + 1 < $c; $k++) { - if (is_array($subtokens[$k]) || !is_array($subtokens[++$k])) { - return false; - } - $subpattern = $this->replaceNamedArguments(implode(',', $subtokens[$k]), $givenParams, $resultingParams, $map); - $subtokens[$k] = $quote . '{' . $quote . $subpattern . $quote . '}' . $quote; - } - $token[2] = implode('', $subtokens); - } - $tokens[$i] = $quote . '{' . $quote . implode(',', $token) . $quote . '}' . $quote; - } - return implode('', $tokens); - } + // replace named arguments + if (($tokens = self::tokenizePattern($pattern)) === false) { + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; - /** - * Fallback implementation for MessageFormatter::formatMessage - * @param string $pattern The pattern string to insert things into. - * @param array $args The array of values to insert into the format string - * @param string $locale The locale to use for formatting locale-dependent parts - * @return string|boolean The formatted pattern string or `FALSE` if an error occurred - */ - protected function fallbackFormat($pattern, $args, $locale) - { - if (($tokens = self::tokenizePattern($pattern)) === false) { - $this->_errorCode = -1; - $this->_errorMessage = "Message pattern is invalid."; - return false; - } - foreach ($tokens as $i => $token) { - if (is_array($token)) { - if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) { - $this->_errorCode = -1; - $this->_errorMessage = "Message pattern is invalid."; - return false; - } - } - } - return implode('', $tokens); - } + return false; + } + $map = []; + foreach ($tokens as $i => $token) { + if (is_array($token)) { + $param = trim($token[0]); + if (!isset($map[$param])) { + $map[$param] = count($map); + } + $token[0] = $map[$param]; + $tokens[$i] = '{' . implode(',', $token) . '}'; + } + } + $pattern = implode('', $tokens); + $map = array_flip($map); - /** - * Tokenizes a pattern by separating normal text from replaceable patterns - * @param string $pattern patter to tokenize - * @return array|boolean array of tokens or false on failure - */ - private static function tokenizePattern($pattern) - { - $depth = 1; - if (($start = $pos = mb_strpos($pattern, '{')) === false) { - return [$pattern]; - } - $tokens = [mb_substr($pattern, 0, $pos)]; - while (true) { - $open = mb_strpos($pattern, '{', $pos + 1); - $close = mb_strpos($pattern, '}', $pos + 1); - if ($open === false && $close === false) { - break; - } - if ($open === false) { - $open = mb_strlen($pattern); - } - if ($close > $open) { - $depth++; - $pos = $open; - } else { - $depth--; - $pos = $close; - } - if ($depth == 0) { - $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1), 3); - $start = $pos + 1; - $tokens[] = mb_substr($pattern, $start, $open - $start); - $start = $open; - } - } - if ($depth != 0) { - return false; - } - return $tokens; - } + $formatter = new \MessageFormatter($language, $pattern); + if ($formatter === null) { + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; - /** - * Parses a token - * @param array $token the token to parse - * @param array $args arguments to replace - * @param string $locale the locale - * @return bool|string parsed token or false on failure - * @throws \yii\base\NotSupportedException when unsupported formatting is used. - */ - private function parseToken($token, $args, $locale) - { - // parsing pattern based on ICU grammar: - // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + return false; + } + $result = $formatter->parse($message); + if ($result === false) { + $this->_errorCode = $formatter->getErrorCode(); + $this->_errorMessage = $formatter->getErrorMessage(); - $param = trim($token[0]); - if (isset($args[$param])) { - $arg = $args[$param]; - } else { - return '{' . implode(',', $token) . '}'; - } - $type = isset($token[1]) ? trim($token[1]) : 'none'; - switch($type) - { - case 'date': - case 'time': - case 'spellout': - case 'ordinal': - case 'duration': - case 'choice': - case 'selectordinal': - throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); - case 'number': - if (is_int($arg) && (!isset($token[2]) || trim($token[2]) == 'integer')) { - return $arg; - } - throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature."); - case 'none': return $arg; - case 'select': - /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html - selectStyle = (selector '{' message '}')+ - */ - if (!isset($token[2])) { - return false; - } - $select = self::tokenizePattern($token[2]); - $c = count($select); - $message = false; - for ($i = 0; $i + 1 < $c; $i++) { - if (is_array($select[$i]) || !is_array($select[$i + 1])) { - return false; - } - $selector = trim($select[$i++]); - if ($message === false && $selector == 'other' || $selector == $arg) { - $message = implode(',', $select[$i]); - } - } - if ($message !== false) { - return $this->fallbackFormat($message, $args, $locale); - } - break; - case 'plural': - /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html - pluralStyle = [offsetValue] (selector '{' message '}')+ - offsetValue = "offset:" number - selector = explicitValue | keyword - explicitValue = '=' number // adjacent, no white space in between - keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ - message: see MessageFormat - */ - if (!isset($token[2])) { - return false; - } - $plural = self::tokenizePattern($token[2]); - $c = count($plural); - $message = false; - $offset = 0; - for ($i = 0; $i + 1 < $c; $i++) { - if (is_array($plural[$i]) || !is_array($plural[$i + 1])) { - return false; - } - $selector = trim($plural[$i++]); - if ($i == 1 && substr($selector, 0, 7) == 'offset:') { - $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7)) - 7)); - $selector = trim(mb_substr($selector, $pos + 1)); - } - if ($message === false && $selector == 'other' || - $selector[0] == '=' && (int) mb_substr($selector, 1) == $arg || - $selector == 'one' && $arg - $offset == 1 - ) { - $message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); - } - } - if ($message !== false) { - return $this->fallbackFormat($message, $args, $locale); - } - break; - } - return false; - } + return false; + } else { + $values = []; + foreach ($result as $key => $value) { + $values[$map[$key]] = $value; + } + + return $values; + } + } + + /** + * Replace named placeholders with numeric placeholders and quote unused. + * + * @param string $pattern The pattern string to replace things into. + * @param array $givenParams The array of values to insert into the format string. + * @param array $resultingParams Modified array of parameters. + * @param array $map + * @return string The pattern string with placeholders replaced. + */ + private function replaceNamedArguments($pattern, $givenParams, &$resultingParams, &$map = []) + { + if (($tokens = self::tokenizePattern($pattern)) === false) { + return false; + } + foreach ($tokens as $i => $token) { + if (!is_array($token)) { + continue; + } + $param = trim($token[0]); + if (isset($givenParams[$param])) { + // if param is given, replace it with a number + if (!isset($map[$param])) { + $map[$param] = count($map); + // make sure only used params are passed to format method + $resultingParams[$map[$param]] = $givenParams[$param]; + } + $token[0] = $map[$param]; + $quote = ""; + } else { + // quote unused token + $quote = "'"; + } + $type = isset($token[1]) ? trim($token[1]) : 'none'; + // replace plural and select format recursively + if ($type == 'plural' || $type == 'select') { + if (!isset($token[2])) { + return false; + } + $subtokens = self::tokenizePattern($token[2]); + $c = count($subtokens); + for ($k = 0; $k + 1 < $c; $k++) { + if (is_array($subtokens[$k]) || !is_array($subtokens[++$k])) { + return false; + } + $subpattern = $this->replaceNamedArguments(implode(',', $subtokens[$k]), $givenParams, $resultingParams, $map); + $subtokens[$k] = $quote . '{' . $quote . $subpattern . $quote . '}' . $quote; + } + $token[2] = implode('', $subtokens); + } + $tokens[$i] = $quote . '{' . $quote . implode(',', $token) . $quote . '}' . $quote; + } + + return implode('', $tokens); + } + + /** + * Fallback implementation for MessageFormatter::formatMessage + * @param string $pattern The pattern string to insert things into. + * @param array $args The array of values to insert into the format string + * @param string $locale The locale to use for formatting locale-dependent parts + * @return string|boolean The formatted pattern string or `FALSE` if an error occurred + */ + protected function fallbackFormat($pattern, $args, $locale) + { + if (($tokens = self::tokenizePattern($pattern)) === false) { + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; + + return false; + } + foreach ($tokens as $i => $token) { + if (is_array($token)) { + if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) { + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; + + return false; + } + } + } + + return implode('', $tokens); + } + + /** + * Tokenizes a pattern by separating normal text from replaceable patterns + * @param string $pattern patter to tokenize + * @return array|boolean array of tokens or false on failure + */ + private static function tokenizePattern($pattern) + { + $depth = 1; + if (($start = $pos = mb_strpos($pattern, '{')) === false) { + return [$pattern]; + } + $tokens = [mb_substr($pattern, 0, $pos)]; + while (true) { + $open = mb_strpos($pattern, '{', $pos + 1); + $close = mb_strpos($pattern, '}', $pos + 1); + if ($open === false && $close === false) { + break; + } + if ($open === false) { + $open = mb_strlen($pattern); + } + if ($close > $open) { + $depth++; + $pos = $open; + } else { + $depth--; + $pos = $close; + } + if ($depth == 0) { + $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1), 3); + $start = $pos + 1; + $tokens[] = mb_substr($pattern, $start, $open - $start); + $start = $open; + } + } + if ($depth != 0) { + return false; + } + + return $tokens; + } + + /** + * Parses a token + * @param array $token the token to parse + * @param array $args arguments to replace + * @param string $locale the locale + * @return bool|string parsed token or false on failure + * @throws \yii\base\NotSupportedException when unsupported formatting is used. + */ + private function parseToken($token, $args, $locale) + { + // parsing pattern based on ICU grammar: + // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + + $param = trim($token[0]); + if (isset($args[$param])) { + $arg = $args[$param]; + } else { + return '{' . implode(',', $token) . '}'; + } + $type = isset($token[1]) ? trim($token[1]) : 'none'; + switch ($type) { + case 'date': + case 'time': + case 'spellout': + case 'ordinal': + case 'duration': + case 'choice': + case 'selectordinal': + throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); + case 'number': + if (is_int($arg) && (!isset($token[2]) || trim($token[2]) == 'integer')) { + return $arg; + } + throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature."); + case 'none': return $arg; + case 'select': + /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html + selectStyle = (selector '{' message '}')+ + */ + if (!isset($token[2])) { + return false; + } + $select = self::tokenizePattern($token[2]); + $c = count($select); + $message = false; + for ($i = 0; $i + 1 < $c; $i++) { + if (is_array($select[$i]) || !is_array($select[$i + 1])) { + return false; + } + $selector = trim($select[$i++]); + if ($message === false && $selector == 'other' || $selector == $arg) { + $message = implode(',', $select[$i]); + } + } + if ($message !== false) { + return $this->fallbackFormat($message, $args, $locale); + } + break; + case 'plural': + /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html + pluralStyle = [offsetValue] (selector '{' message '}')+ + offsetValue = "offset:" number + selector = explicitValue | keyword + explicitValue = '=' number // adjacent, no white space in between + keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ + message: see MessageFormat + */ + if (!isset($token[2])) { + return false; + } + $plural = self::tokenizePattern($token[2]); + $c = count($plural); + $message = false; + $offset = 0; + for ($i = 0; $i + 1 < $c; $i++) { + if (is_array($plural[$i]) || !is_array($plural[$i + 1])) { + return false; + } + $selector = trim($plural[$i++]); + if ($i == 1 && substr($selector, 0, 7) == 'offset:') { + $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7)) - 7)); + $selector = trim(mb_substr($selector, $pos + 1)); + } + if ($message === false && $selector == 'other' || + $selector[0] == '=' && (int) mb_substr($selector, 1) == $arg || + $selector == 'one' && $arg - $offset == 1 + ) { + $message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); + } + } + if ($message !== false) { + return $this->fallbackFormat($message, $args, $locale); + } + break; + } + + return false; + } } diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php index 13c580d9457..a14cdda2721 100644 --- a/framework/i18n/MessageSource.php +++ b/framework/i18n/MessageSource.php @@ -22,102 +22,103 @@ */ class MessageSource extends Component { - /** - * @event MissingTranslationEvent an event that is triggered when a message translation is not found. - */ - const EVENT_MISSING_TRANSLATION = 'missingTranslation'; + /** + * @event MissingTranslationEvent an event that is triggered when a message translation is not found. + */ + const EVENT_MISSING_TRANSLATION = 'missingTranslation'; - /** - * @var boolean whether to force message translation when the source and target languages are the same. - * Defaults to false, meaning translation is only performed when source and target languages are different. - */ - public $forceTranslation = false; - /** - * @var string the language that the original messages are in. If not set, it will use the value of - * [[\yii\base\Application::sourceLanguage]]. - */ - public $sourceLanguage; + /** + * @var boolean whether to force message translation when the source and target languages are the same. + * Defaults to false, meaning translation is only performed when source and target languages are different. + */ + public $forceTranslation = false; + /** + * @var string the language that the original messages are in. If not set, it will use the value of + * [[\yii\base\Application::sourceLanguage]]. + */ + public $sourceLanguage; - private $_messages = []; + private $_messages = []; - /** - * Initializes this component. - */ - public function init() - { - parent::init(); - if ($this->sourceLanguage === null) { - $this->sourceLanguage = Yii::$app->sourceLanguage; - } - } + /** + * Initializes this component. + */ + public function init() + { + parent::init(); + if ($this->sourceLanguage === null) { + $this->sourceLanguage = Yii::$app->sourceLanguage; + } + } - /** - * Loads the message translation for the specified language and category. - * If translation for specific locale code such as `en-US` isn't found it - * tries more generic `en`. - * - * @param string $category the message category - * @param string $language the target language - * @return array the loaded messages. The keys are original messages, and the values - * are translated messages. - */ - protected function loadMessages($category, $language) - { - return []; - } + /** + * Loads the message translation for the specified language and category. + * If translation for specific locale code such as `en-US` isn't found it + * tries more generic `en`. + * + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + return []; + } - /** - * Translates a message to the specified language. - * - * Note that unless [[forceTranslation]] is true, if the target language - * is the same as the [[sourceLanguage|source language]], the message - * will NOT be translated. - * - * If a translation is not found, a [[EVENT_MISSING_TRANSLATION|missingTranslation]] event will be triggered. - * - * @param string $category the message category - * @param string $message the message to be translated - * @param string $language the target language - * @return string|boolean the translated message or false if translation wasn't found or isn't required - */ - public function translate($category, $message, $language) - { - if ($this->forceTranslation || $language !== $this->sourceLanguage) { - return $this->translateMessage($category, $message, $language); - } else { - return false; - } - } + /** + * Translates a message to the specified language. + * + * Note that unless [[forceTranslation]] is true, if the target language + * is the same as the [[sourceLanguage|source language]], the message + * will NOT be translated. + * + * If a translation is not found, a [[EVENT_MISSING_TRANSLATION|missingTranslation]] event will be triggered. + * + * @param string $category the message category + * @param string $message the message to be translated + * @param string $language the target language + * @return string|boolean the translated message or false if translation wasn't found or isn't required + */ + public function translate($category, $message, $language) + { + if ($this->forceTranslation || $language !== $this->sourceLanguage) { + return $this->translateMessage($category, $message, $language); + } else { + return false; + } + } - /** - * Translates the specified message. - * If the message is not found, a [[EVENT_MISSING_TRANSLATION|missingTranslation]] event will be triggered. - * If there is an event handler, it may provide a [[MissingTranslationEvent::$translatedMessage|fallback translation]]. - * If no fallback translation is provided this method will return `false`. - * @param string $category the category that the message belongs to. - * @param string $message the message to be translated. - * @param string $language the target language. - * @return string|boolean the translated message or false if translation wasn't found. - */ - protected function translateMessage($category, $message, $language) - { - $key = $language . '/' . $category; - if (!isset($this->_messages[$key])) { - $this->_messages[$key] = $this->loadMessages($category, $language); - } - if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { - return $this->_messages[$key][$message]; - } elseif ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) { - $event = new MissingTranslationEvent([ - 'category' => $category, - 'message' => $message, - 'language' => $language, - ]); - $this->trigger(self::EVENT_MISSING_TRANSLATION, $event); - if ($event->translatedMessage !== null) { - return $this->_messages[$key][$message] = $event->translatedMessage; - } - } - return $this->_messages[$key][$message] = false; - } + /** + * Translates the specified message. + * If the message is not found, a [[EVENT_MISSING_TRANSLATION|missingTranslation]] event will be triggered. + * If there is an event handler, it may provide a [[MissingTranslationEvent::$translatedMessage|fallback translation]]. + * If no fallback translation is provided this method will return `false`. + * @param string $category the category that the message belongs to. + * @param string $message the message to be translated. + * @param string $language the target language. + * @return string|boolean the translated message or false if translation wasn't found. + */ + protected function translateMessage($category, $message, $language) + { + $key = $language . '/' . $category; + if (!isset($this->_messages[$key])) { + $this->_messages[$key] = $this->loadMessages($category, $language); + } + if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { + return $this->_messages[$key][$message]; + } elseif ($this->hasEventHandlers(self::EVENT_MISSING_TRANSLATION)) { + $event = new MissingTranslationEvent([ + 'category' => $category, + 'message' => $message, + 'language' => $language, + ]); + $this->trigger(self::EVENT_MISSING_TRANSLATION, $event); + if ($event->translatedMessage !== null) { + return $this->_messages[$key][$message] = $event->translatedMessage; + } + } + + return $this->_messages[$key][$message] = false; + } } diff --git a/framework/i18n/MissingTranslationEvent.php b/framework/i18n/MissingTranslationEvent.php index 47f165421ab..cfed6093415 100644 --- a/framework/i18n/MissingTranslationEvent.php +++ b/framework/i18n/MissingTranslationEvent.php @@ -17,22 +17,22 @@ */ class MissingTranslationEvent extends Event { - /** - * @var string the message to be translated. An event handler may use this to provide a fallback translation - * and set [[translatedMessage]] if possible. - */ - public $message; - /** - * @var string the translated message. An event handler may overwrite this property - * with a translated version of [[message]] if possible. If not set (null), it means the message is not translated. - */ - public $translatedMessage; - /** - * @var string the category that the message belongs to - */ - public $category; - /** - * @var string the language ID (e.g. en-US) that the message is to be translated to - */ - public $language; + /** + * @var string the message to be translated. An event handler may use this to provide a fallback translation + * and set [[translatedMessage]] if possible. + */ + public $message; + /** + * @var string the translated message. An event handler may overwrite this property + * with a translated version of [[message]] if possible. If not set (null), it means the message is not translated. + */ + public $translatedMessage; + /** + * @var string the category that the message belongs to + */ + public $category; + /** + * @var string the language ID (e.g. en-US) that the message is to be translated to + */ + public $language; } diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 78665112bf8..ea736fd5f14 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -33,97 +33,100 @@ */ class PhpMessageSource extends MessageSource { - /** - * @var string the base path for all translated messages. Defaults to null, meaning - * the "messages" subdirectory of the application directory (e.g. "protected/messages"). - */ - public $basePath = '@app/messages'; - /** - * @var array mapping between message categories and the corresponding message file paths. - * The file paths are relative to [[basePath]]. For example, - * - * ~~~ - * [ - * 'core' => 'core.php', - * 'ext' => 'extensions.php', - * ] - * ~~~ - */ - public $fileMap; + /** + * @var string the base path for all translated messages. Defaults to null, meaning + * the "messages" subdirectory of the application directory (e.g. "protected/messages"). + */ + public $basePath = '@app/messages'; + /** + * @var array mapping between message categories and the corresponding message file paths. + * The file paths are relative to [[basePath]]. For example, + * + * ~~~ + * [ + * 'core' => 'core.php', + * 'ext' => 'extensions.php', + * ] + * ~~~ + */ + public $fileMap; - /** - * Loads the message translation for the specified language and category. - * If translation for specific locale code such as `en-US` isn't found it - * tries more generic `en`. - * - * @param string $category the message category - * @param string $language the target language - * @return array the loaded messages. The keys are original messages, and the values - * are translated messages. - */ - protected function loadMessages($category, $language) - { - $messageFile = $this->getMessageFilePath($category, $language); - $messages = $this->loadMessagesFromFile($messageFile); + /** + * Loads the message translation for the specified language and category. + * If translation for specific locale code such as `en-US` isn't found it + * tries more generic `en`. + * + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + $messageFile = $this->getMessageFilePath($category, $language); + $messages = $this->loadMessagesFromFile($messageFile); - $fallbackLanguage = substr($language, 0, 2); - if ($fallbackLanguage != $language) { - $fallbackMessageFile = $this->getMessageFilePath($category, $fallbackLanguage); - $fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile); + $fallbackLanguage = substr($language, 0, 2); + if ($fallbackLanguage != $language) { + $fallbackMessageFile = $this->getMessageFilePath($category, $fallbackLanguage); + $fallbackMessages = $this->loadMessagesFromFile($fallbackMessageFile); - if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) { - Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); - } elseif (empty($messages)) { - return $fallbackMessages; - } elseif (!empty($fallbackMessages)) { - foreach ($fallbackMessages as $key => $value) { - if (!empty($value) && empty($messages[$key])) { - $messages[$key] = $fallbackMessages[$key]; - } - } - } - } else { - if ($messages === null) { - Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); - } - } - return (array)$messages; - } + if ($messages === null && $fallbackMessages === null && $fallbackLanguage != $this->sourceLanguage) { + Yii::error("The message file for category '$category' does not exist: $messageFile Fallback file does not exist as well: $fallbackMessageFile", __METHOD__); + } elseif (empty($messages)) { + return $fallbackMessages; + } elseif (!empty($fallbackMessages)) { + foreach ($fallbackMessages as $key => $value) { + if (!empty($value) && empty($messages[$key])) { + $messages[$key] = $fallbackMessages[$key]; + } + } + } + } else { + if ($messages === null) { + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); + } + } - /** - * Returns message file path for the specified language and category. - * - * @param string $category the message category - * @param string $language the target language - * @return string path to message file - */ - protected function getMessageFilePath($category, $language) - { - $messageFile = Yii::getAlias($this->basePath) . "/$language/"; - if (isset($this->fileMap[$category])) { - $messageFile .= $this->fileMap[$category]; - } else { - $messageFile .= str_replace('\\', '/', $category) . '.php'; - } - return $messageFile; - } + return (array) $messages; + } - /** - * Loads the message translation for the specified language and category or returns null if file doesn't exist. - * - * @param $messageFile string path to message file - * @return array|null array of messages or null if file not found - */ - protected function loadMessagesFromFile($messageFile) - { - if (is_file($messageFile)) { - $messages = include($messageFile); - if (!is_array($messages)) { - $messages = []; - } - return $messages; - } else { - return null; - } - } + /** + * Returns message file path for the specified language and category. + * + * @param string $category the message category + * @param string $language the target language + * @return string path to message file + */ + protected function getMessageFilePath($category, $language) + { + $messageFile = Yii::getAlias($this->basePath) . "/$language/"; + if (isset($this->fileMap[$category])) { + $messageFile .= $this->fileMap[$category]; + } else { + $messageFile .= str_replace('\\', '/', $category) . '.php'; + } + + return $messageFile; + } + + /** + * Loads the message translation for the specified language and category or returns null if file doesn't exist. + * + * @param $messageFile string path to message file + * @return array|null array of messages or null if file not found + */ + protected function loadMessagesFromFile($messageFile) + { + if (is_file($messageFile)) { + $messages = include($messageFile); + if (!is_array($messages)) { + $messages = []; + } + + return $messages; + } else { + return null; + } + } } diff --git a/framework/log/DbTarget.php b/framework/log/DbTarget.php index 59eedc63f33..93221294ee8 100644 --- a/framework/log/DbTarget.php +++ b/framework/log/DbTarget.php @@ -22,70 +22,70 @@ */ class DbTarget extends Target { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbTarget object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; - /** - * @var string name of the DB table to store cache content. - * The table should be pre-created as follows: - * - * ~~~ - * CREATE TABLE tbl_log ( - * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - * level INTEGER, - * category VARCHAR(255), - * log_time INTEGER, - * message TEXT, - * INDEX idx_log_level (level), - * INDEX idx_log_category (category) - * ) - * ~~~ - * - * Note that the 'id' column must be created as an auto-incremental column. - * The above SQL uses the MySQL syntax. If you are using other DBMS, you need - * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. - * - * The indexes declared above are not required. They are mainly used to improve the performance - * of some queries about message levels and categories. Depending on your actual needs, you may - * want to create additional indexes (e.g. index on `log_time`). - */ - public $logTable = '{{%log}}'; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbTarget object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_log ( + * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + * level INTEGER, + * category VARCHAR(255), + * log_time INTEGER, + * message TEXT, + * INDEX idx_log_level (level), + * INDEX idx_log_category (category) + * ) + * ~~~ + * + * Note that the 'id' column must be created as an auto-incremental column. + * The above SQL uses the MySQL syntax. If you are using other DBMS, you need + * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. + * + * The indexes declared above are not required. They are mainly used to improve the performance + * of some queries about message levels and categories. Depending on your actual needs, you may + * want to create additional indexes (e.g. index on `log_time`). + */ + public $logTable = '{{%log}}'; - /** - * Initializes the DbTarget component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); - } - } + /** + * Initializes the DbTarget component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); + } + } - /** - * Stores log messages to DB. - */ - public function export() - { - $tableName = $this->db->quoteTableName($this->logTable); - $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) - VALUES (:level, :category, :log_time, :message)"; - $command = $this->db->createCommand($sql); - foreach ($this->messages as $message) { - $command->bindValues([ - ':level' => $message[1], - ':category' => $message[2], - ':log_time' => $message[3], - ':message' => $message[0], - ])->execute(); - } - } + /** + * Stores log messages to DB. + */ + public function export() + { + $tableName = $this->db->quoteTableName($this->logTable); + $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) + VALUES (:level, :category, :log_time, :message)"; + $command = $this->db->createCommand($sql); + foreach ($this->messages as $message) { + $command->bindValues([ + ':level' => $message[1], + ':category' => $message[2], + ':log_time' => $message[3], + ':message' => $message[0], + ])->execute(); + } + } } diff --git a/framework/log/EmailTarget.php b/framework/log/EmailTarget.php index c4e936adf89..e29ad662892 100644 --- a/framework/log/EmailTarget.php +++ b/framework/log/EmailTarget.php @@ -22,60 +22,61 @@ */ class EmailTarget extends Target { - /** - * @var array the configuration array for creating a [[\yii\mail\MessageInterface|message]] object. - * Note that the "to" option must be set, which specifies the destination email address(es). - */ - public $message = []; - /** - * @var MailerInterface|string the mailer object or the application component ID of the mailer object. - * After the EmailTarget object is created, if you want to change this property, you should only assign it - * with a mailer object. - */ - public $mail = 'mail'; + /** + * @var array the configuration array for creating a [[\yii\mail\MessageInterface|message]] object. + * Note that the "to" option must be set, which specifies the destination email address(es). + */ + public $message = []; + /** + * @var MailerInterface|string the mailer object or the application component ID of the mailer object. + * After the EmailTarget object is created, if you want to change this property, you should only assign it + * with a mailer object. + */ + public $mail = 'mail'; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (empty($this->message['to'])) { - throw new InvalidConfigException('The "to" option must be set for EmailTarget::message.'); - } - if (is_string($this->mail)) { - $this->mail = Yii::$app->getComponent($this->mail); - } - if (!$this->mail instanceof MailerInterface) { - throw new InvalidConfigException("EmailTarget::mailer must be either a mailer object or the application component ID of a mailer object."); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (empty($this->message['to'])) { + throw new InvalidConfigException('The "to" option must be set for EmailTarget::message.'); + } + if (is_string($this->mail)) { + $this->mail = Yii::$app->getComponent($this->mail); + } + if (!$this->mail instanceof MailerInterface) { + throw new InvalidConfigException("EmailTarget::mailer must be either a mailer object or the application component ID of a mailer object."); + } + } - /** - * Sends log messages to specified email addresses. - */ - public function export() - { - // moved initialization of subject here because of the following issue - // https://github.com/yiisoft/yii2/issues/1446 - if (empty($this->message['subject'])) { - $this->message['subject'] = 'Application Log'; - } - $messages = array_map([$this, 'formatMessage'], $this->messages); - $body = wordwrap(implode("\n", $messages), 70); - $this->composeMessage($body)->send($this->mail); - } + /** + * Sends log messages to specified email addresses. + */ + public function export() + { + // moved initialization of subject here because of the following issue + // https://github.com/yiisoft/yii2/issues/1446 + if (empty($this->message['subject'])) { + $this->message['subject'] = 'Application Log'; + } + $messages = array_map([$this, 'formatMessage'], $this->messages); + $body = wordwrap(implode("\n", $messages), 70); + $this->composeMessage($body)->send($this->mail); + } - /** - * Composes a mail message with the given body content. - * @param string $body the body content - * @return \yii\mail\MessageInterface $message - */ - protected function composeMessage($body) - { - $message = $this->mail->compose(); - Yii::configure($message, $this->message); - $message->setTextBody($body); - return $message; - } + /** + * Composes a mail message with the given body content. + * @param string $body the body content + * @return \yii\mail\MessageInterface $message + */ + protected function composeMessage($body) + { + $message = $this->mail->compose(); + Yii::configure($message, $this->message); + $message->setTextBody($body); + + return $message; + } } diff --git a/framework/log/FileTarget.php b/framework/log/FileTarget.php index 8058b0c6408..2666d0d7e81 100644 --- a/framework/log/FileTarget.php +++ b/framework/log/FileTarget.php @@ -25,103 +25,102 @@ */ class FileTarget extends Target { - /** - * @var string log file path or path alias. If not set, it will use the "@runtime/logs/app.log" file. - * The directory containing the log files will be automatically created if not existing. - */ - public $logFile; - /** - * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. - */ - public $maxFileSize = 10240; // in KB - /** - * @var integer number of log files used for rotation. Defaults to 5. - */ - public $maxLogFiles = 5; - /** - * @var integer the permission to be set for newly created log files. - * This value will be used by PHP chmod() function. No umask will be applied. - * If not set, the permission will be determined by the current environment. - */ - public $fileMode; - /** - * @var integer the permission to be set for newly created directories. - * This value will be used by PHP chmod() function. No umask will be applied. - * Defaults to 0775, meaning the directory is read-writable by owner and group, - * but read-only for other users. - */ - public $dirMode = 0775; + /** + * @var string log file path or path alias. If not set, it will use the "@runtime/logs/app.log" file. + * The directory containing the log files will be automatically created if not existing. + */ + public $logFile; + /** + * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. + */ + public $maxFileSize = 10240; // in KB + /** + * @var integer number of log files used for rotation. Defaults to 5. + */ + public $maxLogFiles = 5; + /** + * @var integer the permission to be set for newly created log files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + /** + * Initializes the route. + * This method is invoked after the route is created by the route manager. + */ + public function init() + { + parent::init(); + if ($this->logFile === null) { + $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; + } else { + $this->logFile = Yii::getAlias($this->logFile); + } + $logPath = dirname($this->logFile); + if (!is_dir($logPath)) { + FileHelper::createDirectory($logPath, $this->dirMode, true); + } + if ($this->maxLogFiles < 1) { + $this->maxLogFiles = 1; + } + if ($this->maxFileSize < 1) { + $this->maxFileSize = 1; + } + } - /** - * Initializes the route. - * This method is invoked after the route is created by the route manager. - */ - public function init() - { - parent::init(); - if ($this->logFile === null) { - $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; - } else { - $this->logFile = Yii::getAlias($this->logFile); - } - $logPath = dirname($this->logFile); - if (!is_dir($logPath)) { - FileHelper::createDirectory($logPath, $this->dirMode, true); - } - if ($this->maxLogFiles < 1) { - $this->maxLogFiles = 1; - } - if ($this->maxFileSize < 1) { - $this->maxFileSize = 1; - } - } + /** + * Writes log messages to a file. + * @throws InvalidConfigException if unable to open the log file for writing + */ + public function export() + { + $text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n"; + if (($fp = @fopen($this->logFile, 'a')) === false) { + throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); + } + @flock($fp, LOCK_EX); + if (@filesize($this->logFile) > $this->maxFileSize * 1024) { + $this->rotateFiles(); + @flock($fp, LOCK_UN); + @fclose($fp); + @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); + } else { + @fwrite($fp, $text); + @flock($fp, LOCK_UN); + @fclose($fp); + } + if ($this->fileMode !== null) { + @chmod($this->logFile, $this->fileMode); + } + } - /** - * Writes log messages to a file. - * @throws InvalidConfigException if unable to open the log file for writing - */ - public function export() - { - $text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n"; - if (($fp = @fopen($this->logFile, 'a')) === false) { - throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); - } - @flock($fp, LOCK_EX); - if (@filesize($this->logFile) > $this->maxFileSize * 1024) { - $this->rotateFiles(); - @flock($fp, LOCK_UN); - @fclose($fp); - @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); - } else { - @fwrite($fp, $text); - @flock($fp, LOCK_UN); - @fclose($fp); - } - if ($this->fileMode !== null) { - @chmod($this->logFile, $this->fileMode); - } - } - - /** - * Rotates log files. - */ - protected function rotateFiles() - { - $file = $this->logFile; - for ($i = $this->maxLogFiles; $i > 0; --$i) { - $rotateFile = $file . '.' . $i; - if (is_file($rotateFile)) { - // suppress errors because it's possible multiple processes enter into this section - if ($i === $this->maxLogFiles) { - @unlink($rotateFile); - } else { - @rename($rotateFile, $file . '.' . ($i + 1)); - } - } - } - if (is_file($file)) { - @rename($file, $file . '.1'); // suppress errors because it's possible multiple processes enter into this section - } - } + /** + * Rotates log files. + */ + protected function rotateFiles() + { + $file = $this->logFile; + for ($i = $this->maxLogFiles; $i > 0; --$i) { + $rotateFile = $file . '.' . $i; + if (is_file($rotateFile)) { + // suppress errors because it's possible multiple processes enter into this section + if ($i === $this->maxLogFiles) { + @unlink($rotateFile); + } else { + @rename($rotateFile, $file . '.' . ($i + 1)); + } + } + } + if (is_file($file)) { + @rename($file, $file . '.1'); // suppress errors because it's possible multiple processes enter into this section + } + } } diff --git a/framework/log/Logger.php b/framework/log/Logger.php index 38da3e90416..f25c530fb52 100644 --- a/framework/log/Logger.php +++ b/framework/log/Logger.php @@ -74,281 +74,284 @@ */ class Logger extends Component { - /** - * Error message level. An error message is one that indicates the abnormal termination of the - * application and may require developer's handling. - */ - const LEVEL_ERROR = 0x01; - /** - * Warning message level. A warning message is one that indicates some abnormal happens but - * the application is able to continue to run. Developers should pay attention to this message. - */ - const LEVEL_WARNING = 0x02; - /** - * Informational message level. An informational message is one that includes certain information - * for developers to review. - */ - const LEVEL_INFO = 0x04; - /** - * Tracing message level. An tracing message is one that reveals the code execution flow. - */ - const LEVEL_TRACE = 0x08; - /** - * Profiling message level. This indicates the message is for profiling purpose. - */ - const LEVEL_PROFILE = 0x40; - /** - * Profiling message level. This indicates the message is for profiling purpose. It marks the - * beginning of a profiling block. - */ - const LEVEL_PROFILE_BEGIN = 0x50; - /** - * Profiling message level. This indicates the message is for profiling purpose. It marks the - * end of a profiling block. - */ - const LEVEL_PROFILE_END = 0x60; + /** + * Error message level. An error message is one that indicates the abnormal termination of the + * application and may require developer's handling. + */ + const LEVEL_ERROR = 0x01; + /** + * Warning message level. A warning message is one that indicates some abnormal happens but + * the application is able to continue to run. Developers should pay attention to this message. + */ + const LEVEL_WARNING = 0x02; + /** + * Informational message level. An informational message is one that includes certain information + * for developers to review. + */ + const LEVEL_INFO = 0x04; + /** + * Tracing message level. An tracing message is one that reveals the code execution flow. + */ + const LEVEL_TRACE = 0x08; + /** + * Profiling message level. This indicates the message is for profiling purpose. + */ + const LEVEL_PROFILE = 0x40; + /** + * Profiling message level. This indicates the message is for profiling purpose. It marks the + * beginning of a profiling block. + */ + const LEVEL_PROFILE_BEGIN = 0x50; + /** + * Profiling message level. This indicates the message is for profiling purpose. It marks the + * end of a profiling block. + */ + const LEVEL_PROFILE_END = 0x60; - /** - * @var array logged messages. This property is managed by [[log()]] and [[flush()]]. - * Each log message is of the following structure: - * - * ~~~ - * [ - * [0] => message (mixed, can be a string or some complex data, such as an exception object) - * [1] => level (integer) - * [2] => category (string) - * [3] => timestamp (float, obtained by microtime(true)) - * [4] => traces (array, debug backtrace, contains the application code call stacks) - * ] - * ~~~ - */ - public $messages = []; - /** - * @var array debug data. This property stores various types of debug data reported at - * different instrument places. - */ - public $data = []; - /** - * @var array|Target[] the log targets. Each array element represents a single [[Target|log target]] instance - * or the configuration for creating the log target instance. - */ - public $targets = []; - /** - * @var integer how many messages should be logged before they are flushed from memory and sent to targets. - * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged. - * Set this property to be 0 if you don't want to flush messages until the application terminates. - * This property mainly affects how much memory will be taken by the logged messages. - * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]]. - */ - public $flushInterval = 1000; - /** - * @var integer how much call stack information (file name and line number) should be logged for each message. - * If it is greater than 0, at most that number of call stacks will be logged. Note that only application - * call stacks are counted. - * - * If not set, it will default to 3 when `YII_ENV` is set as "dev", and 0 otherwise. - */ - public $traceLevel; + /** + * @var array logged messages. This property is managed by [[log()]] and [[flush()]]. + * Each log message is of the following structure: + * + * ~~~ + * [ + * [0] => message (mixed, can be a string or some complex data, such as an exception object) + * [1] => level (integer) + * [2] => category (string) + * [3] => timestamp (float, obtained by microtime(true)) + * [4] => traces (array, debug backtrace, contains the application code call stacks) + * ] + * ~~~ + */ + public $messages = []; + /** + * @var array debug data. This property stores various types of debug data reported at + * different instrument places. + */ + public $data = []; + /** + * @var array|Target[] the log targets. Each array element represents a single [[Target|log target]] instance + * or the configuration for creating the log target instance. + */ + public $targets = []; + /** + * @var integer how many messages should be logged before they are flushed from memory and sent to targets. + * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged. + * Set this property to be 0 if you don't want to flush messages until the application terminates. + * This property mainly affects how much memory will be taken by the logged messages. + * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]]. + */ + public $flushInterval = 1000; + /** + * @var integer how much call stack information (file name and line number) should be logged for each message. + * If it is greater than 0, at most that number of call stacks will be logged. Note that only application + * call stacks are counted. + * + * If not set, it will default to 3 when `YII_ENV` is set as "dev", and 0 otherwise. + */ + public $traceLevel; - /** - * Initializes the logger by registering [[flush()]] as a shutdown function. - */ - public function init() - { - parent::init(); - if ($this->traceLevel === null) { - $this->traceLevel = YII_ENV_DEV ? 3 : 0; - } - foreach ($this->targets as $name => $target) { - if (!$target instanceof Target) { - $this->targets[$name] = Yii::createObject($target); - } - } - register_shutdown_function([$this, 'flush'], true); - } + /** + * Initializes the logger by registering [[flush()]] as a shutdown function. + */ + public function init() + { + parent::init(); + if ($this->traceLevel === null) { + $this->traceLevel = YII_ENV_DEV ? 3 : 0; + } + foreach ($this->targets as $name => $target) { + if (!$target instanceof Target) { + $this->targets[$name] = Yii::createObject($target); + } + } + register_shutdown_function([$this, 'flush'], true); + } - /** - * Logs a message with the given type and category. - * If [[traceLevel]] is greater than 0, additional call stack information about - * the application code will be logged as well. - * @param string $message the message to be logged. - * @param integer $level the level of the message. This must be one of the following: - * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`, - * `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`. - * @param string $category the category of the message. - */ - public function log($message, $level, $category = 'application') - { - $time = microtime(true); - $traces = []; - if ($this->traceLevel > 0) { - $count = 0; - $ts = debug_backtrace(); - array_pop($ts); // remove the last trace since it would be the entry script, not very useful - foreach ($ts as $trace) { - if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) { - unset($trace['object'], $trace['args']); - $traces[] = $trace; - if (++$count >= $this->traceLevel) { - break; - } - } - } - } - $this->messages[] = [$message, $level, $category, $time, $traces]; - if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) { - $this->flush(); - } - } + /** + * Logs a message with the given type and category. + * If [[traceLevel]] is greater than 0, additional call stack information about + * the application code will be logged as well. + * @param string $message the message to be logged. + * @param integer $level the level of the message. This must be one of the following: + * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`, + * `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`. + * @param string $category the category of the message. + */ + public function log($message, $level, $category = 'application') + { + $time = microtime(true); + $traces = []; + if ($this->traceLevel > 0) { + $count = 0; + $ts = debug_backtrace(); + array_pop($ts); // remove the last trace since it would be the entry script, not very useful + foreach ($ts as $trace) { + if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) { + unset($trace['object'], $trace['args']); + $traces[] = $trace; + if (++$count >= $this->traceLevel) { + break; + } + } + } + } + $this->messages[] = [$message, $level, $category, $time, $traces]; + if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) { + $this->flush(); + } + } - /** - * Flushes log messages from memory to targets. - * @param boolean $final whether this is a final call during a request. - */ - public function flush($final = false) - { - /** @var Target $target */ - foreach ($this->targets as $target) { - if ($target->enabled) { - $target->collect($this->messages, $final); - } - } - $this->messages = []; - } + /** + * Flushes log messages from memory to targets. + * @param boolean $final whether this is a final call during a request. + */ + public function flush($final = false) + { + /** @var Target $target */ + foreach ($this->targets as $target) { + if ($target->enabled) { + $target->collect($this->messages, $final); + } + } + $this->messages = []; + } - /** - * Returns the total elapsed time since the start of the current request. - * This method calculates the difference between now and the timestamp - * defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning - * of [[\yii\BaseYii]] class file. - * @return float the total elapsed time in seconds for current request. - */ - public function getElapsedTime() - { - return microtime(true) - YII_BEGIN_TIME; - } + /** + * Returns the total elapsed time since the start of the current request. + * This method calculates the difference between now and the timestamp + * defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning + * of [[\yii\BaseYii]] class file. + * @return float the total elapsed time in seconds for current request. + */ + public function getElapsedTime() + { + return microtime(true) - YII_BEGIN_TIME; + } - /** - * Returns the profiling results. - * - * By default, all profiling results will be returned. You may provide - * `$categories` and `$excludeCategories` as parameters to retrieve the - * results that you are interested in. - * - * @param array $categories list of categories that you are interested in. - * You can use an asterisk at the end of a category to do a prefix match. - * For example, 'yii\db\*' will match categories starting with 'yii\db\', - * such as 'yii\db\Connection'. - * @param array $excludeCategories list of categories that you want to exclude - * @return array the profiling results. Each element is an array consisting of these elements: - * `info`, `category`, `timestamp`, `trace`, `level`, `duration`. - */ - public function getProfiling($categories = [], $excludeCategories = []) - { - $timings = $this->calculateTimings($this->messages); - if (empty($categories) && empty($excludeCategories)) { - return $timings; - } + /** + * Returns the profiling results. + * + * By default, all profiling results will be returned. You may provide + * `$categories` and `$excludeCategories` as parameters to retrieve the + * results that you are interested in. + * + * @param array $categories list of categories that you are interested in. + * You can use an asterisk at the end of a category to do a prefix match. + * For example, 'yii\db\*' will match categories starting with 'yii\db\', + * such as 'yii\db\Connection'. + * @param array $excludeCategories list of categories that you want to exclude + * @return array the profiling results. Each element is an array consisting of these elements: + * `info`, `category`, `timestamp`, `trace`, `level`, `duration`. + */ + public function getProfiling($categories = [], $excludeCategories = []) + { + $timings = $this->calculateTimings($this->messages); + if (empty($categories) && empty($excludeCategories)) { + return $timings; + } - foreach ($timings as $i => $timing) { - $matched = empty($categories); - foreach ($categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($timing['category'], $prefix) === 0 && ($timing['category'] === $category || $prefix !== $category)) { - $matched = true; - break; - } - } + foreach ($timings as $i => $timing) { + $matched = empty($categories); + foreach ($categories as $category) { + $prefix = rtrim($category, '*'); + if (strpos($timing['category'], $prefix) === 0 && ($timing['category'] === $category || $prefix !== $category)) { + $matched = true; + break; + } + } - if ($matched) { - foreach ($excludeCategories as $category) { - $prefix = rtrim($category, '*'); - foreach ($timings as $i => $timing) { - if (strpos($timing['category'], $prefix) === 0 && ($timing['category'] === $category || $prefix !== $category)) { - $matched = false; - break; - } - } - } - } + if ($matched) { + foreach ($excludeCategories as $category) { + $prefix = rtrim($category, '*'); + foreach ($timings as $i => $timing) { + if (strpos($timing['category'], $prefix) === 0 && ($timing['category'] === $category || $prefix !== $category)) { + $matched = false; + break; + } + } + } + } - if (!$matched) { - unset($timings[$i]); - } - } - return array_values($timings); - } + if (!$matched) { + unset($timings[$i]); + } + } - /** - * Returns the statistical results of DB queries. - * The results returned include the number of SQL statements executed and - * the total time spent. - * @return array the first element indicates the number of SQL statements executed, - * and the second element the total time spent in SQL execution. - */ - public function getDbProfiling() - { - $timings = $this->getProfiling(['yii\db\Command::query', 'yii\db\Command::execute']); - $count = count($timings); - $time = 0; - foreach ($timings as $timing) { - $time += $timing['duration']; - } - return [$count, $time]; - } + return array_values($timings); + } - /** - * Calculates the elapsed time for the given log messages. - * @param array $messages the log messages obtained from profiling - * @return array timings. Each element is an array consisting of these elements: - * `info`, `category`, `timestamp`, `trace`, `level`, `duration`. - */ - public function calculateTimings($messages) - { - $timings = []; - $stack = []; + /** + * Returns the statistical results of DB queries. + * The results returned include the number of SQL statements executed and + * the total time spent. + * @return array the first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + */ + public function getDbProfiling() + { + $timings = $this->getProfiling(['yii\db\Command::query', 'yii\db\Command::execute']); + $count = count($timings); + $time = 0; + foreach ($timings as $timing) { + $time += $timing['duration']; + } - foreach ($messages as $i => $log) { - list($token, $level, $category, $timestamp, $traces) = $log; - $log[5] = $i; - if ($level == Logger::LEVEL_PROFILE_BEGIN) { - $stack[] = $log; - } elseif ($level == Logger::LEVEL_PROFILE_END) { - if (($last = array_pop($stack)) !== null && $last[0] === $token) { - $timings[$last[5]] = [ - 'info' => $last[0], - 'category' => $last[2], - 'timestamp' => $last[3], - 'trace' => $last[4], - 'level' => count($stack), - 'duration' => $timestamp - $last[3], - ]; - } - } - } + return [$count, $time]; + } - ksort($timings); + /** + * Calculates the elapsed time for the given log messages. + * @param array $messages the log messages obtained from profiling + * @return array timings. Each element is an array consisting of these elements: + * `info`, `category`, `timestamp`, `trace`, `level`, `duration`. + */ + public function calculateTimings($messages) + { + $timings = []; + $stack = []; - return array_values($timings); - } + foreach ($messages as $i => $log) { + list($token, $level, $category, $timestamp, $traces) = $log; + $log[5] = $i; + if ($level == Logger::LEVEL_PROFILE_BEGIN) { + $stack[] = $log; + } elseif ($level == Logger::LEVEL_PROFILE_END) { + if (($last = array_pop($stack)) !== null && $last[0] === $token) { + $timings[$last[5]] = [ + 'info' => $last[0], + 'category' => $last[2], + 'timestamp' => $last[3], + 'trace' => $last[4], + 'level' => count($stack), + 'duration' => $timestamp - $last[3], + ]; + } + } + } + ksort($timings); - /** - * Returns the text display of the specified level. - * @param integer $level the message level, e.g. [[LEVEL_ERROR]], [[LEVEL_WARNING]]. - * @return string the text display of the level - */ - public static function getLevelName($level) - { - static $levels = [ - self::LEVEL_ERROR => 'error', - self::LEVEL_WARNING => 'warning', - self::LEVEL_INFO => 'info', - self::LEVEL_TRACE => 'trace', - self::LEVEL_PROFILE_BEGIN => 'profile begin', - self::LEVEL_PROFILE_END => 'profile end', - ]; - return isset($levels[$level]) ? $levels[$level] : 'unknown'; - } + return array_values($timings); + } + + + /** + * Returns the text display of the specified level. + * @param integer $level the message level, e.g. [[LEVEL_ERROR]], [[LEVEL_WARNING]]. + * @return string the text display of the level + */ + public static function getLevelName($level) + { + static $levels = [ + self::LEVEL_ERROR => 'error', + self::LEVEL_WARNING => 'warning', + self::LEVEL_INFO => 'info', + self::LEVEL_TRACE => 'trace', + self::LEVEL_PROFILE_BEGIN => 'profile begin', + self::LEVEL_PROFILE_END => 'profile end', + ]; + + return isset($levels[$level]) ? $levels[$level] : 'unknown'; + } } diff --git a/framework/log/Target.php b/framework/log/Target.php index d5d8e5b9b0d..4bd2d5dee5d 100644 --- a/framework/log/Target.php +++ b/framework/log/Target.php @@ -31,208 +31,210 @@ */ abstract class Target extends Component { - /** - * @var boolean whether to enable this log target. Defaults to true. - */ - public $enabled = true; - /** - * @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories. - * You can use an asterisk at the end of a category so that the category may be used to - * match those categories sharing the same common prefix. For example, 'yii\db\*' will match - * categories starting with 'yii\db\', such as 'yii\db\Connection'. - */ - public $categories = []; - /** - * @var array list of message categories that this target is NOT interested in. Defaults to empty, meaning no uninteresting messages. - * If this property is not empty, then any category listed here will be excluded from [[categories]]. - * You can use an asterisk at the end of a category so that the category can be used to - * match those categories sharing the same common prefix. For example, 'yii\db\*' will match - * categories starting with 'yii\db\', such as 'yii\db\Connection'. - * @see categories - */ - public $except = []; - /** - * @var boolean whether to log a message containing the current user name and ID. Defaults to false. - * @see \yii\web\User - */ - public $logUser = false; - /** - * @var array list of the PHP predefined variables that should be logged in a message. - * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged. - * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`. - */ - public $logVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']; - /** - * @var integer how many messages should be accumulated before they are exported. - * Defaults to 1000. Note that messages will always be exported when the application terminates. - * Set this property to be 0 if you don't want to export messages until the application terminates. - */ - public $exportInterval = 1000; - /** - * @var array the messages that are retrieved from the logger so far by this log target. - * Please refer to [[Logger::messages]] for the details about the message structure. - */ - public $messages = []; - - private $_levels = 0; - - /** - * Exports log [[messages]] to a specific destination. - * Child classes must implement this method. - */ - abstract public function export(); - - /** - * Processes the given log messages. - * This method will filter the given messages with [[levels]] and [[categories]]. - * And if requested, it will also export the filtering result to specific medium (e.g. email). - * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure - * of each message. - * @param boolean $final whether this method is called at the end of the current application - */ - public function collect($messages, $final) - { - $this->messages = array_merge($this->messages, $this->filterMessages($messages, $this->getLevels(), $this->categories, $this->except)); - $count = count($this->messages); - if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { - if (($context = $this->getContextMessage()) !== '') { - $this->messages[] = [$context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME]; - } - $this->export(); - $this->messages = []; - } - } - - /** - * Generates the context information to be logged. - * The default implementation will dump user information, system variables, etc. - * @return string the context information. If an empty string, it means no context information. - */ - protected function getContextMessage() - { - $context = []; - if ($this->logUser && ($user = Yii::$app->getComponent('user', false)) !== null) { - /** @var \yii\web\User $user */ - $context[] = 'User: ' . $user->getId(); - } - - foreach ($this->logVars as $name) { - if (!empty($GLOBALS[$name])) { - $context[] = "\${$name} = " . var_export($GLOBALS[$name], true); - } - } - - return implode("\n\n", $context); - } - - /** - * @return integer the message levels that this target is interested in. This is a bitmap of - * level values. Defaults to 0, meaning all available levels. - */ - public function getLevels() - { - return $this->_levels; - } - - /** - * Sets the message levels that this target is interested in. - * - * The parameter can be either an array of interested level names or an integer representing - * the bitmap of the interested level values. Valid level names include: 'error', - * 'warning', 'info', 'trace' and 'profile'; valid level values include: - * [[Logger::LEVEL_ERROR]], [[Logger::LEVEL_WARNING]], [[Logger::LEVEL_INFO]], - * [[Logger::LEVEL_TRACE]] and [[Logger::LEVEL_PROFILE]]. - * - * For example, - * - * ~~~ - * ['error', 'warning'] - * // which is equivalent to: - * Logger::LEVEL_ERROR | Logger::LEVEL_WARNING - * ~~~ - * - * @param array|integer $levels message levels that this target is interested in. - * @throws InvalidConfigException if an unknown level name is given - */ - public function setLevels($levels) - { - static $levelMap = [ - 'error' => Logger::LEVEL_ERROR, - 'warning' => Logger::LEVEL_WARNING, - 'info' => Logger::LEVEL_INFO, - 'trace' => Logger::LEVEL_TRACE, - 'profile' => Logger::LEVEL_PROFILE, - ]; - if (is_array($levels)) { - $this->_levels = 0; - foreach ($levels as $level) { - if (isset($levelMap[$level])) { - $this->_levels |= $levelMap[$level]; - } else { - throw new InvalidConfigException("Unrecognized level: $level"); - } - } - } else { - $this->_levels = $levels; - } - } - - /** - * Filters the given messages according to their categories and levels. - * @param array $messages messages to be filtered - * @param integer $levels the message levels to filter by. This is a bitmap of - * level values. Value 0 means allowing all levels. - * @param array $categories the message categories to filter by. If empty, it means all categories are allowed. - * @param array $except the message categories to exclude. If empty, it means all categories are allowed. - * @return array the filtered messages. - */ - public static function filterMessages($messages, $levels = 0, $categories = [], $except = []) - { - foreach ($messages as $i => $message) { - if ($levels && !($levels & $message[1])) { - unset($messages[$i]); - continue; - } - - $matched = empty($categories); - foreach ($categories as $category) { - if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { - $matched = true; - break; - } - } - - if ($matched) { - foreach ($except as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { - $matched = false; - break; - } - } - } - - if (!$matched) { - unset($messages[$i]); - } - } - return $messages; - } - - /** - * Formats a log message. - * The message structure follows that in [[Logger::messages]]. - * @param array $message the log message to be formatted. - * @return string the formatted message - */ - public function formatMessage($message) - { - list($text, $level, $category, $timestamp) = $message; - $level = Logger::getLevelName($level); - if (!is_string($text)) { - $text = var_export($text, true); - } - $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; - return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text"; - } + /** + * @var boolean whether to enable this log target. Defaults to true. + */ + public $enabled = true; + /** + * @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories. + * You can use an asterisk at the end of a category so that the category may be used to + * match those categories sharing the same common prefix. For example, 'yii\db\*' will match + * categories starting with 'yii\db\', such as 'yii\db\Connection'. + */ + public $categories = []; + /** + * @var array list of message categories that this target is NOT interested in. Defaults to empty, meaning no uninteresting messages. + * If this property is not empty, then any category listed here will be excluded from [[categories]]. + * You can use an asterisk at the end of a category so that the category can be used to + * match those categories sharing the same common prefix. For example, 'yii\db\*' will match + * categories starting with 'yii\db\', such as 'yii\db\Connection'. + * @see categories + */ + public $except = []; + /** + * @var boolean whether to log a message containing the current user name and ID. Defaults to false. + * @see \yii\web\User + */ + public $logUser = false; + /** + * @var array list of the PHP predefined variables that should be logged in a message. + * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged. + * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`. + */ + public $logVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']; + /** + * @var integer how many messages should be accumulated before they are exported. + * Defaults to 1000. Note that messages will always be exported when the application terminates. + * Set this property to be 0 if you don't want to export messages until the application terminates. + */ + public $exportInterval = 1000; + /** + * @var array the messages that are retrieved from the logger so far by this log target. + * Please refer to [[Logger::messages]] for the details about the message structure. + */ + public $messages = []; + + private $_levels = 0; + + /** + * Exports log [[messages]] to a specific destination. + * Child classes must implement this method. + */ + abstract public function export(); + + /** + * Processes the given log messages. + * This method will filter the given messages with [[levels]] and [[categories]]. + * And if requested, it will also export the filtering result to specific medium (e.g. email). + * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure + * of each message. + * @param boolean $final whether this method is called at the end of the current application + */ + public function collect($messages, $final) + { + $this->messages = array_merge($this->messages, $this->filterMessages($messages, $this->getLevels(), $this->categories, $this->except)); + $count = count($this->messages); + if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { + if (($context = $this->getContextMessage()) !== '') { + $this->messages[] = [$context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME]; + } + $this->export(); + $this->messages = []; + } + } + + /** + * Generates the context information to be logged. + * The default implementation will dump user information, system variables, etc. + * @return string the context information. If an empty string, it means no context information. + */ + protected function getContextMessage() + { + $context = []; + if ($this->logUser && ($user = Yii::$app->getComponent('user', false)) !== null) { + /** @var \yii\web\User $user */ + $context[] = 'User: ' . $user->getId(); + } + + foreach ($this->logVars as $name) { + if (!empty($GLOBALS[$name])) { + $context[] = "\${$name} = " . var_export($GLOBALS[$name], true); + } + } + + return implode("\n\n", $context); + } + + /** + * @return integer the message levels that this target is interested in. This is a bitmap of + * level values. Defaults to 0, meaning all available levels. + */ + public function getLevels() + { + return $this->_levels; + } + + /** + * Sets the message levels that this target is interested in. + * + * The parameter can be either an array of interested level names or an integer representing + * the bitmap of the interested level values. Valid level names include: 'error', + * 'warning', 'info', 'trace' and 'profile'; valid level values include: + * [[Logger::LEVEL_ERROR]], [[Logger::LEVEL_WARNING]], [[Logger::LEVEL_INFO]], + * [[Logger::LEVEL_TRACE]] and [[Logger::LEVEL_PROFILE]]. + * + * For example, + * + * ~~~ + * ['error', 'warning'] + * // which is equivalent to: + * Logger::LEVEL_ERROR | Logger::LEVEL_WARNING + * ~~~ + * + * @param array|integer $levels message levels that this target is interested in. + * @throws InvalidConfigException if an unknown level name is given + */ + public function setLevels($levels) + { + static $levelMap = [ + 'error' => Logger::LEVEL_ERROR, + 'warning' => Logger::LEVEL_WARNING, + 'info' => Logger::LEVEL_INFO, + 'trace' => Logger::LEVEL_TRACE, + 'profile' => Logger::LEVEL_PROFILE, + ]; + if (is_array($levels)) { + $this->_levels = 0; + foreach ($levels as $level) { + if (isset($levelMap[$level])) { + $this->_levels |= $levelMap[$level]; + } else { + throw new InvalidConfigException("Unrecognized level: $level"); + } + } + } else { + $this->_levels = $levels; + } + } + + /** + * Filters the given messages according to their categories and levels. + * @param array $messages messages to be filtered + * @param integer $levels the message levels to filter by. This is a bitmap of + * level values. Value 0 means allowing all levels. + * @param array $categories the message categories to filter by. If empty, it means all categories are allowed. + * @param array $except the message categories to exclude. If empty, it means all categories are allowed. + * @return array the filtered messages. + */ + public static function filterMessages($messages, $levels = 0, $categories = [], $except = []) + { + foreach ($messages as $i => $message) { + if ($levels && !($levels & $message[1])) { + unset($messages[$i]); + continue; + } + + $matched = empty($categories); + foreach ($categories as $category) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { + $matched = true; + break; + } + } + + if ($matched) { + foreach ($except as $category) { + $prefix = rtrim($category, '*'); + if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + $matched = false; + break; + } + } + } + + if (!$matched) { + unset($messages[$i]); + } + } + + return $messages; + } + + /** + * Formats a log message. + * The message structure follows that in [[Logger::messages]]. + * @param array $message the log message to be formatted. + * @return string the formatted message + */ + public function formatMessage($message) + { + list($text, $level, $category, $timestamp) = $message; + $level = Logger::getLevelName($level); + if (!is_string($text)) { + $text = var_export($text, true); + } + $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; + + return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text"; + } } diff --git a/framework/mail/BaseMailer.php b/framework/mail/BaseMailer.php index 34f81044a60..d40951bdc5a 100644 --- a/framework/mail/BaseMailer.php +++ b/framework/mail/BaseMailer.php @@ -29,323 +29,332 @@ */ abstract class BaseMailer extends Component implements MailerInterface, ViewContextInterface { - /** - * @event \yii\base\MailEvent an event raised right before send. - * You may set [[\yii\base\MailEvent::isValid]] to be false to cancel the send. - */ - const EVENT_BEFORE_SEND = 'beforeSend'; - /** - * @event \yii\base\MailEvent an event raised right after send. - */ - const EVENT_AFTER_SEND = 'afterSend'; - /** - * @var string directory containing view files for this email messages. - * This can be specified as an absolute path or path alias. - */ - public $viewPath = '@app/mail'; - /** - * @var string|boolean HTML layout view name. This is the layout used to render HTML mail body. - * The property can take the following values: - * - * - a relative view name: a view file relative to [[viewPath]], e.g., 'layouts/html'. - * - a path alias: an absolute view file path specified as a path alias, e.g., '@app/mail/html'. - * - a boolean false: the layout is disabled. - */ - public $htmlLayout = 'layouts/html'; - /** - * @var string|boolean text layout view name. This is the layout used to render TEXT mail body. - * Please refer to [[htmlLayout]] for possible values that this property can take. - */ - public $textLayout = 'layouts/text'; - /** - * @var array the configuration that should be applied to any newly created - * email message instance by [[createMessage()]] or [[compose()]]. Any valid property defined - * by [[MessageInterface]] can be configured, such as `from`, `to`, `subject`, `textBody`, `htmlBody`, etc. - * - * For example: - * - * ~~~ - * [ - * 'charset' => 'UTF-8', - * 'from' => 'noreply@mydomain.com', - * 'bcc' => 'developer@mydomain.com', - * ] - * ~~~ - */ - public $messageConfig = []; - /** - * @var string the default class name of the new message instances created by [[createMessage()]] - */ - public $messageClass = 'yii\mail\BaseMessage'; - /** - * @var boolean whether to save email messages as files under [[fileTransportPath]] instead of sending them - * to the actual recipients. This is usually used during development for debugging purpose. - * @see fileTransportPath - */ - public $useFileTransport = false; - /** - * @var string the directory where the email messages are saved when [[useFileTransport]] is true. - */ - public $fileTransportPath = '@runtime/mail'; - /** - * @var callable a PHP callback that will be called by [[send()]] when [[useFileTransport]] is true. - * The callback should return a file name which will be used to save the email message. - * If not set, the file name will be generated based on the current timestamp. - * - * The signature of the callback is: - * - * ~~~ - * function ($mailer, $message) - * ~~~ - */ - public $fileTransportCallback; + /** + * @event \yii\base\MailEvent an event raised right before send. + * You may set [[\yii\base\MailEvent::isValid]] to be false to cancel the send. + */ + const EVENT_BEFORE_SEND = 'beforeSend'; + /** + * @event \yii\base\MailEvent an event raised right after send. + */ + const EVENT_AFTER_SEND = 'afterSend'; + /** + * @var string directory containing view files for this email messages. + * This can be specified as an absolute path or path alias. + */ + public $viewPath = '@app/mail'; + /** + * @var string|boolean HTML layout view name. This is the layout used to render HTML mail body. + * The property can take the following values: + * + * - a relative view name: a view file relative to [[viewPath]], e.g., 'layouts/html'. + * - a path alias: an absolute view file path specified as a path alias, e.g., '@app/mail/html'. + * - a boolean false: the layout is disabled. + */ + public $htmlLayout = 'layouts/html'; + /** + * @var string|boolean text layout view name. This is the layout used to render TEXT mail body. + * Please refer to [[htmlLayout]] for possible values that this property can take. + */ + public $textLayout = 'layouts/text'; + /** + * @var array the configuration that should be applied to any newly created + * email message instance by [[createMessage()]] or [[compose()]]. Any valid property defined + * by [[MessageInterface]] can be configured, such as `from`, `to`, `subject`, `textBody`, `htmlBody`, etc. + * + * For example: + * + * ~~~ + * [ + * 'charset' => 'UTF-8', + * 'from' => 'noreply@mydomain.com', + * 'bcc' => 'developer@mydomain.com', + * ] + * ~~~ + */ + public $messageConfig = []; + /** + * @var string the default class name of the new message instances created by [[createMessage()]] + */ + public $messageClass = 'yii\mail\BaseMessage'; + /** + * @var boolean whether to save email messages as files under [[fileTransportPath]] instead of sending them + * to the actual recipients. This is usually used during development for debugging purpose. + * @see fileTransportPath + */ + public $useFileTransport = false; + /** + * @var string the directory where the email messages are saved when [[useFileTransport]] is true. + */ + public $fileTransportPath = '@runtime/mail'; + /** + * @var callable a PHP callback that will be called by [[send()]] when [[useFileTransport]] is true. + * The callback should return a file name which will be used to save the email message. + * If not set, the file name will be generated based on the current timestamp. + * + * The signature of the callback is: + * + * ~~~ + * function ($mailer, $message) + * ~~~ + */ + public $fileTransportCallback; - /** - * @var \yii\base\View|array view instance or its array configuration. - */ - private $_view = []; + /** + * @var \yii\base\View|array view instance or its array configuration. + */ + private $_view = []; - /** - * @param array|View $view view instance or its array configuration that will be used to - * render message bodies. - * @throws InvalidConfigException on invalid argument. - */ - public function setView($view) - { - if (!is_array($view) && !is_object($view)) { - throw new InvalidConfigException('"' . get_class($this) . '::view" should be either object or configuration array, "' . gettype($view) . '" given.'); - } - $this->_view = $view; - } + /** + * @param array|View $view view instance or its array configuration that will be used to + * render message bodies. + * @throws InvalidConfigException on invalid argument. + */ + public function setView($view) + { + if (!is_array($view) && !is_object($view)) { + throw new InvalidConfigException('"' . get_class($this) . '::view" should be either object or configuration array, "' . gettype($view) . '" given.'); + } + $this->_view = $view; + } - /** - * @return View view instance. - */ - public function getView() - { - if (!is_object($this->_view)) { - $this->_view = $this->createView($this->_view); - } - return $this->_view; - } + /** + * @return View view instance. + */ + public function getView() + { + if (!is_object($this->_view)) { + $this->_view = $this->createView($this->_view); + } - /** - * Creates view instance from given configuration. - * @param array $config view configuration. - * @return View view instance. - */ - protected function createView(array $config) - { - if (!array_key_exists('class', $config)) { - $config['class'] = View::className(); - } - return Yii::createObject($config); - } + return $this->_view; + } - /** - * Creates a new message instance and optionally composes its body content via view rendering. - * - * @param string|array $view the view to be used for rendering the message body. This can be: - * - * - a string, which represents the view name or path alias for rendering the HTML body of the email. - * In this case, the text body will be generated by applying `strip_tags()` to the HTML body. - * - an array with 'html' and/or 'text' elements. The 'html' element refers to the view name or path alias - * for rendering the HTML body, while 'text' element is for rendering the text body. For example, - * `['html' => 'contact-html', 'text' => 'contact-text']`. - * - null, meaning the message instance will be returned without body content. - * - * The view to be rendered can be specified in one of the following formats: - * - * - path alias (e.g. "@app/mail/contact"); - * - a relative view name (e.g. "contact"): the actual view file will be resolved by [[findViewFile()]] - * - * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @return MessageInterface message instance. - */ - public function compose($view = null, array $params = []) - { - $message = $this->createMessage(); - if ($view !== null) { - $params['message'] = $message; - if (is_array($view)) { - if (isset($view['html'])) { - $html = $this->render($view['html'], $params, $this->htmlLayout); - } - if (isset($view['text'])) { - $text = $this->render($view['text'], $params, $this->textLayout); - } - } else { - $html = $this->render($view, $params, $this->htmlLayout); - } - if (isset($html)) { - $message->setHtmlBody($html); - } - if (isset($text)) { - $message->setTextBody($text); - } elseif (isset($html)) { - $message->setTextBody(strip_tags($html)); - } - } - return $message; - } + /** + * Creates view instance from given configuration. + * @param array $config view configuration. + * @return View view instance. + */ + protected function createView(array $config) + { + if (!array_key_exists('class', $config)) { + $config['class'] = View::className(); + } - /** - * Creates a new message instance. - * The newly created instance will be initialized with the configuration specified by [[messageConfig]]. - * If the configuration does not specify a 'class', the [[messageClass]] will be used as the class - * of the new message instance. - * @return MessageInterface message instance. - */ - protected function createMessage() - { - $config = $this->messageConfig; - if (!array_key_exists('class', $config)) { - $config['class'] = $this->messageClass; - } - return Yii::createObject($config); - } + return Yii::createObject($config); + } - /** - * Sends the given email message. - * This method will log a message about the email being sent. - * If [[useFileTransport]] is true, it will save the email as a file under [[fileTransportPath]]. - * Otherwise, it will call [[sendMessage()]] to send the email to its recipient(s). - * Child classes should implement [[sendMessage()]] with the actual email sending logic. - * @param MessageInterface $message email message instance to be sent - * @return boolean whether the message has been sent successfully - */ - public function send($message) - { - if (!$this->beforeSend($message)) { - return false; - } + /** + * Creates a new message instance and optionally composes its body content via view rendering. + * + * @param string|array $view the view to be used for rendering the message body. This can be: + * + * - a string, which represents the view name or path alias for rendering the HTML body of the email. + * In this case, the text body will be generated by applying `strip_tags()` to the HTML body. + * - an array with 'html' and/or 'text' elements. The 'html' element refers to the view name or path alias + * for rendering the HTML body, while 'text' element is for rendering the text body. For example, + * `['html' => 'contact-html', 'text' => 'contact-text']`. + * - null, meaning the message instance will be returned without body content. + * + * The view to be rendered can be specified in one of the following formats: + * + * - path alias (e.g. "@app/mail/contact"); + * - a relative view name (e.g. "contact"): the actual view file will be resolved by [[findViewFile()]] + * + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @return MessageInterface message instance. + */ + public function compose($view = null, array $params = []) + { + $message = $this->createMessage(); + if ($view !== null) { + $params['message'] = $message; + if (is_array($view)) { + if (isset($view['html'])) { + $html = $this->render($view['html'], $params, $this->htmlLayout); + } + if (isset($view['text'])) { + $text = $this->render($view['text'], $params, $this->textLayout); + } + } else { + $html = $this->render($view, $params, $this->htmlLayout); + } + if (isset($html)) { + $message->setHtmlBody($html); + } + if (isset($text)) { + $message->setTextBody($text); + } elseif (isset($html)) { + $message->setTextBody(strip_tags($html)); + } + } - $address = $message->getTo(); - if (is_array($address)) { - $address = implode(', ', array_keys($address)); - } - Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__); + return $message; + } - if ($this->useFileTransport) { - $isSuccessful = $this->saveMessage($message); - } else { - $isSuccessful = $this->sendMessage($message); - } - $this->afterSend($message, $isSuccessful); - return $isSuccessful; - } + /** + * Creates a new message instance. + * The newly created instance will be initialized with the configuration specified by [[messageConfig]]. + * If the configuration does not specify a 'class', the [[messageClass]] will be used as the class + * of the new message instance. + * @return MessageInterface message instance. + */ + protected function createMessage() + { + $config = $this->messageConfig; + if (!array_key_exists('class', $config)) { + $config['class'] = $this->messageClass; + } - /** - * Sends multiple messages at once. - * - * The default implementation simply calls [[send()]] multiple times. - * Child classes may override this method to implement more efficient way of - * sending multiple messages. - * - * @param array $messages list of email messages, which should be sent. - * @return integer number of messages that are successfully sent. - */ - public function sendMultiple(array $messages) - { - $successCount = 0; - foreach ($messages as $message) { - if ($this->send($message)) { - $successCount++; - } - } - return $successCount; - } + return Yii::createObject($config); + } - /** - * Renders the specified view with optional parameters and layout. - * The view will be rendered using the [[view]] component. - * @param string $view the view name or the path alias of the view file. - * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param string|boolean $layout layout view name or path alias. If false, no layout will be applied. - * @return string the rendering result. - */ - public function render($view, $params = [], $layout = false) - { - $output = $this->getView()->render($view, $params, $this); - if ($layout !== false) { - return $this->getView()->render($layout, ['content' => $output], $this); - } else { - return $output; - } - } + /** + * Sends the given email message. + * This method will log a message about the email being sent. + * If [[useFileTransport]] is true, it will save the email as a file under [[fileTransportPath]]. + * Otherwise, it will call [[sendMessage()]] to send the email to its recipient(s). + * Child classes should implement [[sendMessage()]] with the actual email sending logic. + * @param MessageInterface $message email message instance to be sent + * @return boolean whether the message has been sent successfully + */ + public function send($message) + { + if (!$this->beforeSend($message)) { + return false; + } - /** - * Sends the specified message. - * This method should be implemented by child classes with the actual email sending logic. - * @param MessageInterface $message the message to be sent - * @return boolean whether the message is sent successfully - */ - abstract protected function sendMessage($message); + $address = $message->getTo(); + if (is_array($address)) { + $address = implode(', ', array_keys($address)); + } + Yii::info('Sending email "' . $message->getSubject() . '" to "' . $address . '"', __METHOD__); - /** - * Saves the message as a file under [[fileTransportPath]]. - * @param MessageInterface $message - * @return boolean whether the message is saved successfully - */ - protected function saveMessage($message) - { - $path = Yii::getAlias($this->fileTransportPath); - if (!is_dir(($path))) { - mkdir($path, 0777, true); - } - if ($this->fileTransportCallback !== null) { - $file = $path . '/' . call_user_func($this->fileTransportCallback, $this, $message); - } else { - $file = $path . '/' . $this->generateMessageFileName(); - } - file_put_contents($file, $message->toString()); - return true; - } + if ($this->useFileTransport) { + $isSuccessful = $this->saveMessage($message); + } else { + $isSuccessful = $this->sendMessage($message); + } + $this->afterSend($message, $isSuccessful); - /** - * @return string the file name for saving the message when [[useFileTransport]] is true. - */ - public function generateMessageFileName() - { - $time = microtime(true); - return date('Ymd-His-', $time) . sprintf('%04d', (int)(($time - (int)$time) * 10000)) . '-' . sprintf('%04d', mt_rand(0, 10000)) . '.eml'; - } + return $isSuccessful; + } - /** - * Finds the view file corresponding to the specified relative view name. - * This method will return the view file by prefixing the view name with [[viewPath]]. - * @param string $view a relative view name. The name does NOT start with a slash. - * @return string the view file path. Note that the file may not exist. - */ - public function findViewFile($view) - { - return Yii::getAlias($this->viewPath) . DIRECTORY_SEPARATOR . $view; - } + /** + * Sends multiple messages at once. + * + * The default implementation simply calls [[send()]] multiple times. + * Child classes may override this method to implement more efficient way of + * sending multiple messages. + * + * @param array $messages list of email messages, which should be sent. + * @return integer number of messages that are successfully sent. + */ + public function sendMultiple(array $messages) + { + $successCount = 0; + foreach ($messages as $message) { + if ($this->send($message)) { + $successCount++; + } + } - /** - * This method is invoked right before mail send. - * You may override this method to do last-minute preparation for the message. - * If you override this method, please make sure you call the parent implementation first. - * @param MessageInterface $message - * @return boolean whether to continue sending an email. - */ - public function beforeSend($message) - { - $event = new MailEvent(['message' => $message]); - $this->trigger(self::EVENT_BEFORE_SEND, $event); - return $event->isValid; - } + return $successCount; + } - /** - * This method is invoked right after mail was send. - * You may override this method to do some postprocessing or logging based on mail send status. - * If you override this method, please make sure you call the parent implementation first. - * @param MessageInterface $message - * @param boolean $isSuccessful - */ - public function afterSend($message, $isSuccessful) - { - $event = new MailEvent(['message' => $message, 'isSuccessful' => $isSuccessful]); - $this->trigger(self::EVENT_AFTER_SEND, $event); - } + /** + * Renders the specified view with optional parameters and layout. + * The view will be rendered using the [[view]] component. + * @param string $view the view name or the path alias of the view file. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param string|boolean $layout layout view name or path alias. If false, no layout will be applied. + * @return string the rendering result. + */ + public function render($view, $params = [], $layout = false) + { + $output = $this->getView()->render($view, $params, $this); + if ($layout !== false) { + return $this->getView()->render($layout, ['content' => $output], $this); + } else { + return $output; + } + } + + /** + * Sends the specified message. + * This method should be implemented by child classes with the actual email sending logic. + * @param MessageInterface $message the message to be sent + * @return boolean whether the message is sent successfully + */ + abstract protected function sendMessage($message); + + /** + * Saves the message as a file under [[fileTransportPath]]. + * @param MessageInterface $message + * @return boolean whether the message is saved successfully + */ + protected function saveMessage($message) + { + $path = Yii::getAlias($this->fileTransportPath); + if (!is_dir(($path))) { + mkdir($path, 0777, true); + } + if ($this->fileTransportCallback !== null) { + $file = $path . '/' . call_user_func($this->fileTransportCallback, $this, $message); + } else { + $file = $path . '/' . $this->generateMessageFileName(); + } + file_put_contents($file, $message->toString()); + + return true; + } + + /** + * @return string the file name for saving the message when [[useFileTransport]] is true. + */ + public function generateMessageFileName() + { + $time = microtime(true); + + return date('Ymd-His-', $time) . sprintf('%04d', (int) (($time - (int) $time) * 10000)) . '-' . sprintf('%04d', mt_rand(0, 10000)) . '.eml'; + } + + /** + * Finds the view file corresponding to the specified relative view name. + * This method will return the view file by prefixing the view name with [[viewPath]]. + * @param string $view a relative view name. The name does NOT start with a slash. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view) + { + return Yii::getAlias($this->viewPath) . DIRECTORY_SEPARATOR . $view; + } + + /** + * This method is invoked right before mail send. + * You may override this method to do last-minute preparation for the message. + * If you override this method, please make sure you call the parent implementation first. + * @param MessageInterface $message + * @return boolean whether to continue sending an email. + */ + public function beforeSend($message) + { + $event = new MailEvent(['message' => $message]); + $this->trigger(self::EVENT_BEFORE_SEND, $event); + + return $event->isValid; + } + + /** + * This method is invoked right after mail was send. + * You may override this method to do some postprocessing or logging based on mail send status. + * If you override this method, please make sure you call the parent implementation first. + * @param MessageInterface $message + * @param boolean $isSuccessful + */ + public function afterSend($message, $isSuccessful) + { + $event = new MailEvent(['message' => $message, 'isSuccessful' => $isSuccessful]); + $this->trigger(self::EVENT_AFTER_SEND, $event); + } } diff --git a/framework/mail/BaseMessage.php b/framework/mail/BaseMessage.php index 36d328830df..2b7b8320596 100644 --- a/framework/mail/BaseMessage.php +++ b/framework/mail/BaseMessage.php @@ -23,30 +23,32 @@ */ abstract class BaseMessage extends Object implements MessageInterface { - /** - * @inheritdoc - */ - public function send(MailerInterface $mailer = null) - { - if ($mailer === null) { - $mailer = Yii::$app->getMail(); - } - return $mailer->send($this); - } + /** + * @inheritdoc + */ + public function send(MailerInterface $mailer = null) + { + if ($mailer === null) { + $mailer = Yii::$app->getMail(); + } - /** - * PHP magic method that returns the string representation of this object. - * @return string the string representation of this object. - */ - public function __toString() - { - // __toString cannot throw exception - // use trigger_error to bypass this limitation - try { - return $this->toString(); - } catch (\Exception $e) { - trigger_error($e->getMessage() . "\n\n" . $e->getTraceAsString()); - return ''; - } - } + return $mailer->send($this); + } + + /** + * PHP magic method that returns the string representation of this object. + * @return string the string representation of this object. + */ + public function __toString() + { + // __toString cannot throw exception + // use trigger_error to bypass this limitation + try { + return $this->toString(); + } catch (\Exception $e) { + trigger_error($e->getMessage() . "\n\n" . $e->getTraceAsString()); + + return ''; + } + } } diff --git a/framework/mail/MailerInterface.php b/framework/mail/MailerInterface.php index 5ee9ccd029a..49e985024e2 100644 --- a/framework/mail/MailerInterface.php +++ b/framework/mail/MailerInterface.php @@ -28,37 +28,37 @@ */ interface MailerInterface { - /** - * Creates a new message instance and optionally composes its body content via view rendering. - * - * @param string|array $view the view to be used for rendering the message body. This can be: - * - * - a string, which represents the view name or path alias for rendering the HTML body of the email. - * In this case, the text body will be generated by applying `strip_tags()` to the HTML body. - * - an array with 'html' and/or 'text' elements. The 'html' element refers to the view name or path alias - * for rendering the HTML body, while 'text' element is for rendering the text body. For example, - * `['html' => 'contact-html', 'text' => 'contact-text']`. - * - null, meaning the message instance will be returned without body content. - * - * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @return MessageInterface message instance. - */ - public function compose($view = null, array $params = []); + /** + * Creates a new message instance and optionally composes its body content via view rendering. + * + * @param string|array $view the view to be used for rendering the message body. This can be: + * + * - a string, which represents the view name or path alias for rendering the HTML body of the email. + * In this case, the text body will be generated by applying `strip_tags()` to the HTML body. + * - an array with 'html' and/or 'text' elements. The 'html' element refers to the view name or path alias + * for rendering the HTML body, while 'text' element is for rendering the text body. For example, + * `['html' => 'contact-html', 'text' => 'contact-text']`. + * - null, meaning the message instance will be returned without body content. + * + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @return MessageInterface message instance. + */ + public function compose($view = null, array $params = []); - /** - * Sends the given email message. - * @param MessageInterface $message email message instance to be sent - * @return boolean whether the message has been sent successfully - */ - public function send($message); + /** + * Sends the given email message. + * @param MessageInterface $message email message instance to be sent + * @return boolean whether the message has been sent successfully + */ + public function send($message); - /** - * Sends multiple messages at once. - * - * This method may be implemented by some mailers which support more efficient way of sending multiple messages in the same batch. - * - * @param array $messages list of email messages, which should be sent. - * @return integer number of messages that are successfully sent. - */ - public function sendMultiple(array $messages); + /** + * Sends multiple messages at once. + * + * This method may be implemented by some mailers which support more efficient way of sending multiple messages in the same batch. + * + * @param array $messages list of email messages, which should be sent. + * @return integer number of messages that are successfully sent. + */ + public function sendMultiple(array $messages); } diff --git a/framework/mail/MessageInterface.php b/framework/mail/MessageInterface.php index 94cfd18db3b..321e8066dcd 100644 --- a/framework/mail/MessageInterface.php +++ b/framework/mail/MessageInterface.php @@ -32,187 +32,187 @@ */ interface MessageInterface { - /** - * Returns the character set of this message. - * @return string the character set of this message. - */ - public function getCharset(); - - /** - * Sets the character set of this message. - * @param string $charset character set name. - * @return static self reference. - */ - public function setCharset($charset); - - /** - * Returns the message sender. - * @return string the sender - */ - public function getFrom(); - - /** - * Sets the message sender. - * @param string|array $from sender email address. - * You may pass an array of addresses if this message is from multiple people. - * You may also specify sender name in addition to email address using format: - * `[email => name]`. - * @return static self reference. - */ - public function setFrom($from); - - /** - * Returns the message recipient(s). - * @return array the message recipients - */ - public function getTo(); - - /** - * Sets the message recipient(s). - * @param string|array $to receiver email address. - * You may pass an array of addresses if multiple recipients should receive this message. - * You may also specify receiver name in addition to email address using format: - * `[email => name]`. - * @return static self reference. - */ - public function setTo($to); - - /** - * Returns the reply-to address of this message. - * @return string the reply-to address of this message. - */ - public function getReplyTo(); - - /** - * Sets the reply-to address of this message. - * @param string|array $replyTo the reply-to address. - * You may pass an array of addresses if this message should be replied to multiple people. - * You may also specify reply-to name in addition to email address using format: - * `[email => name]`. - * @return static self reference. - */ - public function setReplyTo($replyTo); - - /** - * Returns the Cc (additional copy receiver) addresses of this message. - * @return array the Cc (additional copy receiver) addresses of this message. - */ - public function getCc(); - - /** - * Sets the Cc (additional copy receiver) addresses of this message. - * @param string|array $cc copy receiver email address. - * You may pass an array of addresses if multiple recipients should receive this message. - * You may also specify receiver name in addition to email address using format: - * `[email => name]`. - * @return static self reference. - */ - public function setCc($cc); - - /** - * Returns the Bcc (hidden copy receiver) addresses of this message. - * @return array the Bcc (hidden copy receiver) addresses of this message. - */ - public function getBcc(); - - /** - * Sets the Bcc (hidden copy receiver) addresses of this message. - * @param string|array $bcc hidden copy receiver email address. - * You may pass an array of addresses if multiple recipients should receive this message. - * You may also specify receiver name in addition to email address using format: - * `[email => name]`. - * @return static self reference. - */ - public function setBcc($bcc); - - /** - * Returns the message subject. - * @return string the message subject - */ - public function getSubject(); - - /** - * Sets the message subject. - * @param string $subject message subject - * @return static self reference. - */ - public function setSubject($subject); - - /** - * Sets message plain text content. - * @param string $text message plain text content. - * @return static self reference. - */ - public function setTextBody($text); - - /** - * Sets message HTML content. - * @param string $html message HTML content. - * @return static self reference. - */ - public function setHtmlBody($html); - - /** - * Attaches existing file to the email message. - * @param string $fileName full file name - * @param array $options options for embed file. Valid options are: - * - * - fileName: name, which should be used to attach file. - * - contentType: attached file MIME type. - * - * @return static self reference. - */ - public function attach($fileName, array $options = []); - - /** - * Attach specified content as file for the email message. - * @param string $content attachment file content. - * @param array $options options for embed file. Valid options are: - * - * - fileName: name, which should be used to attach file. - * - contentType: attached file MIME type. - * - * @return static self reference. - */ - public function attachContent($content, array $options = []); - - /** - * Attach a file and return it's CID source. - * This method should be used when embedding images or other data in a message. - * @param string $fileName file name. - * @param array $options options for embed file. Valid options are: - * - * - fileName: name, which should be used to attach file. - * - contentType: attached file MIME type. - * - * @return string attachment CID. - */ - public function embed($fileName, array $options = []); - - /** - * Attach a content as file and return it's CID source. - * This method should be used when embedding images or other data in a message. - * @param string $content attachment file content. - * @param array $options options for embed file. Valid options are: - * - * - fileName: name, which should be used to attach file. - * - contentType: attached file MIME type. - * - * @return string attachment CID. - */ - public function embedContent($content, array $options = []); - - /** - * Sends this email message. - * @param MailerInterface $mailer the mailer that should be used to send this message. - * If null, the "mail" application component will be used instead. - * @return boolean whether this message is sent successfully. - */ - public function send(MailerInterface $mailer = null); - - /** - * Returns string representation of this message. - * @return string the string representation of this message. - */ - public function toString(); + /** + * Returns the character set of this message. + * @return string the character set of this message. + */ + public function getCharset(); + + /** + * Sets the character set of this message. + * @param string $charset character set name. + * @return static self reference. + */ + public function setCharset($charset); + + /** + * Returns the message sender. + * @return string the sender + */ + public function getFrom(); + + /** + * Sets the message sender. + * @param string|array $from sender email address. + * You may pass an array of addresses if this message is from multiple people. + * You may also specify sender name in addition to email address using format: + * `[email => name]`. + * @return static self reference. + */ + public function setFrom($from); + + /** + * Returns the message recipient(s). + * @return array the message recipients + */ + public function getTo(); + + /** + * Sets the message recipient(s). + * @param string|array $to receiver email address. + * You may pass an array of addresses if multiple recipients should receive this message. + * You may also specify receiver name in addition to email address using format: + * `[email => name]`. + * @return static self reference. + */ + public function setTo($to); + + /** + * Returns the reply-to address of this message. + * @return string the reply-to address of this message. + */ + public function getReplyTo(); + + /** + * Sets the reply-to address of this message. + * @param string|array $replyTo the reply-to address. + * You may pass an array of addresses if this message should be replied to multiple people. + * You may also specify reply-to name in addition to email address using format: + * `[email => name]`. + * @return static self reference. + */ + public function setReplyTo($replyTo); + + /** + * Returns the Cc (additional copy receiver) addresses of this message. + * @return array the Cc (additional copy receiver) addresses of this message. + */ + public function getCc(); + + /** + * Sets the Cc (additional copy receiver) addresses of this message. + * @param string|array $cc copy receiver email address. + * You may pass an array of addresses if multiple recipients should receive this message. + * You may also specify receiver name in addition to email address using format: + * `[email => name]`. + * @return static self reference. + */ + public function setCc($cc); + + /** + * Returns the Bcc (hidden copy receiver) addresses of this message. + * @return array the Bcc (hidden copy receiver) addresses of this message. + */ + public function getBcc(); + + /** + * Sets the Bcc (hidden copy receiver) addresses of this message. + * @param string|array $bcc hidden copy receiver email address. + * You may pass an array of addresses if multiple recipients should receive this message. + * You may also specify receiver name in addition to email address using format: + * `[email => name]`. + * @return static self reference. + */ + public function setBcc($bcc); + + /** + * Returns the message subject. + * @return string the message subject + */ + public function getSubject(); + + /** + * Sets the message subject. + * @param string $subject message subject + * @return static self reference. + */ + public function setSubject($subject); + + /** + * Sets message plain text content. + * @param string $text message plain text content. + * @return static self reference. + */ + public function setTextBody($text); + + /** + * Sets message HTML content. + * @param string $html message HTML content. + * @return static self reference. + */ + public function setHtmlBody($html); + + /** + * Attaches existing file to the email message. + * @param string $fileName full file name + * @param array $options options for embed file. Valid options are: + * + * - fileName: name, which should be used to attach file. + * - contentType: attached file MIME type. + * + * @return static self reference. + */ + public function attach($fileName, array $options = []); + + /** + * Attach specified content as file for the email message. + * @param string $content attachment file content. + * @param array $options options for embed file. Valid options are: + * + * - fileName: name, which should be used to attach file. + * - contentType: attached file MIME type. + * + * @return static self reference. + */ + public function attachContent($content, array $options = []); + + /** + * Attach a file and return it's CID source. + * This method should be used when embedding images or other data in a message. + * @param string $fileName file name. + * @param array $options options for embed file. Valid options are: + * + * - fileName: name, which should be used to attach file. + * - contentType: attached file MIME type. + * + * @return string attachment CID. + */ + public function embed($fileName, array $options = []); + + /** + * Attach a content as file and return it's CID source. + * This method should be used when embedding images or other data in a message. + * @param string $content attachment file content. + * @param array $options options for embed file. Valid options are: + * + * - fileName: name, which should be used to attach file. + * - contentType: attached file MIME type. + * + * @return string attachment CID. + */ + public function embedContent($content, array $options = []); + + /** + * Sends this email message. + * @param MailerInterface $mailer the mailer that should be used to send this message. + * If null, the "mail" application component will be used instead. + * @return boolean whether this message is sent successfully. + */ + public function send(MailerInterface $mailer = null); + + /** + * Returns string representation of this message. + * @return string the string representation of this message. + */ + public function toString(); } diff --git a/framework/messages/ar/yii.php b/framework/messages/ar/yii.php index 92b219f1177..bcc5b27458d 100644 --- a/framework/messages/ar/yii.php +++ b/framework/messages/ar/yii.php @@ -17,65 +17,65 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return array( - 'the input value' => 'قيمة المُدخل', - '(not set)' => '(لم تحدد)', - 'An internal server error occurred.' => '.حدث خطأ داخلي في الخادم', - 'Are you sure to delete this item?' => 'هل أنت متأكد من حذف هذا العنصر؟', - 'Delete' => 'حذف', - 'Error' => 'خطأ', - 'File upload failed.' => '.فشل في تحميل الملف', - 'Home' => 'الرئيسية', - 'Invalid data received for parameter "{param}".' => 'بيانات غير صالحة قد وردت في "{param}".', - 'Login Required' => 'تسجبل الدخول ضروري', - 'Missing required arguments: {params}' => 'البيانات المطلوبة ضرورية: {params}', - 'Missing required parameters: {params}' => 'البيانات المطلوبة ضرورية: {params}', - 'No' => 'Nein', - 'No help for unknown command "{command}".' => 'ليس هناك مساعدة لأمر غير معروف "{command}".', - 'No help for unknown sub-command "{command}".' => 'ليس هناك مساعدة لأمر فرعي غير معروف "{command}".', - 'No results found.' => 'لا نتائج وجدت.', - 'Only files with these extensions are allowed: {extensions}.' => 'فقط الملفات التي تحمل هذه الصيغ مسموح بها: {extentions}.', - 'Only files with these mimeTypes are allowed: {mimeTypes}.' => 'فقط الملفات التي تحمل هذه الصيغ مسموح بها: {extentions}.', - 'Page not found.' => 'الصفحة غير موجودة', - 'Please fix the following errors:' => 'الرجاء تصحيح الأخطاء التالية:', - 'Please upload a file.' => 'الرجاء تحميل ملف.', - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'عرض {begin, number}-{end, number} من {totalCount, number} {totalCount, plural, one{item} other{items}}.', - 'The file "{file}" is not an image.' => 'الملف "{file}" ليس بصورة.', - 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'الملف "{file}" كبير الحجم. حجمه لا يجب أن يتخطى {limit, number} {limit, plural, one{byte} other{bytes}}.', - 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'الملف "{file}" صغير جداً. حجمه لا يجب أن يكون أصغر من {limit, number} {limit, plural, one{byte} other{bytes}}.', - 'The format of {attribute} is invalid.' => 'شكل {attribute} غير صالح', - 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" كبيرة جداً. ارتفاعها لا يمكن أن يتخطى {limit, number} {limit, plural, one{pixel} other{pixels}}.', - 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" كبيرة جداً. عرضها لا يمكن أن يتخطى {limit, number} {limit, plural, one{pixel} other{pixels}}.', - 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" صغيرة جداً. ارتفاعها لا يمكن أن يقل عن {limit, number} {limit, plural, one{pixel} other{pixels}}.', - 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" كبيرة جداً. عرضها لا يمكن أن يقل عن {limit, number} {limit, plural, one{pixel} other{pixels}}.', - 'The verification code is incorrect.' => 'رمز التحقق غير صحيح', - 'Total {count, number} {count, plural, one{item} other{items}}.' => 'مجموع {count, number} {count, plural, one{item} other{items}}.', - 'Unable to verify your data submission.' => 'لم نستطع التأكد من البيانات المقدمة.', - 'Unknown command "{command}".' => 'أمر غير معروف.', - 'Unknown option: --{name}' => 'خيار غير معروف: --{name}', - 'Update' => 'تحديث', - 'View' => 'عرض', - 'Yes' => 'نعم', - 'You are not allowed to perform this action.' => 'لا تستطيع القيام بهذاالعمل', - 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'تستطيع كأقصى حد تحميل {limit, number} {limit, plural, one{file} other{files}}.', - '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" سبق استعماله', - '{attribute} cannot be blank.' => '{attribute} لا يمكن تركه فارغًا.', - '{attribute} is invalid.' => '{attribute} غير صالح.', - '{attribute} is not a valid URL.' => '{attribute} ليس بعنوان صحيح.', - '{attribute} is not a valid email address.' => '{attribute} ليس ببريد إلكتروني صحيح.', - '{attribute} must be "{requiredValue}".' => '{attribute} يجب أن يكون "{requiredValue}".', - '{attribute} must be a number.' => '{attribute} يجب أن يكون رقمًا', - '{attribute} must be a string.' => '{attribute} يجب أن يكون كلمات', - '{attribute} must be an integer.' => '{attribute} يجب أن يكون رقمًا صحيحًا', - '{attribute} must be either "{true}" or "{false}".' => '{attribute} يجب أن يكن إما "{true}" أو "{false}".', - '{attribute} must be greater than "{compareValue}".' => '{attribute} يجب أن يكون أكبر من "{compareValue}".', - '{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} يجب أن يكون أكبر من أو يساوي "{compareValue}".', - '{attribute} must be less than "{compareValue}".' => '{attribute} يجب أن يكون أصغر من "{compareValue}".', - '{attribute} must be less than or equal to "{compareValue}".' => '{attribute} يجب أن يكون أصغر من أو يساوي "{compareValue}".', - '{attribute} must be no greater than {max}.' => '{attribute} يجب أن لا يكون أكبر من "{compareValue}".', - '{attribute} must be no less than {min}.' => '{attribute} يجب أن لا يكون أصغر من "{compareValue}".', - '{attribute} must be repeated exactly.' => '{attribute} يجب أن يكون متطابق.', - '{attribute} must not be equal to "{compareValue}".' => '{attribute} يجب أن يساوي "{compareValue}"', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} يجب أن يحتوي على أكثر من {min, number} {min, plural, one{character} other{characters}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} يجب أن لا يحتوي على أكثر من {max, number} {max, plural, one{character} other{characters}}.', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} يجب أن يحتوي على {length, number} {length, plural, one{character} other{characters}}.', + 'the input value' => 'قيمة المُدخل', + '(not set)' => '(لم تحدد)', + 'An internal server error occurred.' => '.حدث خطأ داخلي في الخادم', + 'Are you sure to delete this item?' => 'هل أنت متأكد من حذف هذا العنصر؟', + 'Delete' => 'حذف', + 'Error' => 'خطأ', + 'File upload failed.' => '.فشل في تحميل الملف', + 'Home' => 'الرئيسية', + 'Invalid data received for parameter "{param}".' => 'بيانات غير صالحة قد وردت في "{param}".', + 'Login Required' => 'تسجبل الدخول ضروري', + 'Missing required arguments: {params}' => 'البيانات المطلوبة ضرورية: {params}', + 'Missing required parameters: {params}' => 'البيانات المطلوبة ضرورية: {params}', + 'No' => 'Nein', + 'No help for unknown command "{command}".' => 'ليس هناك مساعدة لأمر غير معروف "{command}".', + 'No help for unknown sub-command "{command}".' => 'ليس هناك مساعدة لأمر فرعي غير معروف "{command}".', + 'No results found.' => 'لا نتائج وجدت.', + 'Only files with these extensions are allowed: {extensions}.' => 'فقط الملفات التي تحمل هذه الصيغ مسموح بها: {extentions}.', + 'Only files with these mimeTypes are allowed: {mimeTypes}.' => 'فقط الملفات التي تحمل هذه الصيغ مسموح بها: {extentions}.', + 'Page not found.' => 'الصفحة غير موجودة', + 'Please fix the following errors:' => 'الرجاء تصحيح الأخطاء التالية:', + 'Please upload a file.' => 'الرجاء تحميل ملف.', + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'عرض {begin, number}-{end, number} من {totalCount, number} {totalCount, plural, one{item} other{items}}.', + 'The file "{file}" is not an image.' => 'الملف "{file}" ليس بصورة.', + 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'الملف "{file}" كبير الحجم. حجمه لا يجب أن يتخطى {limit, number} {limit, plural, one{byte} other{bytes}}.', + 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'الملف "{file}" صغير جداً. حجمه لا يجب أن يكون أصغر من {limit, number} {limit, plural, one{byte} other{bytes}}.', + 'The format of {attribute} is invalid.' => 'شكل {attribute} غير صالح', + 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" كبيرة جداً. ارتفاعها لا يمكن أن يتخطى {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" كبيرة جداً. عرضها لا يمكن أن يتخطى {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" صغيرة جداً. ارتفاعها لا يمكن أن يقل عن {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'الصورة "{file}" كبيرة جداً. عرضها لا يمكن أن يقل عن {limit, number} {limit, plural, one{pixel} other{pixels}}.', + 'The verification code is incorrect.' => 'رمز التحقق غير صحيح', + 'Total {count, number} {count, plural, one{item} other{items}}.' => 'مجموع {count, number} {count, plural, one{item} other{items}}.', + 'Unable to verify your data submission.' => 'لم نستطع التأكد من البيانات المقدمة.', + 'Unknown command "{command}".' => 'أمر غير معروف.', + 'Unknown option: --{name}' => 'خيار غير معروف: --{name}', + 'Update' => 'تحديث', + 'View' => 'عرض', + 'Yes' => 'نعم', + 'You are not allowed to perform this action.' => 'لا تستطيع القيام بهذاالعمل', + 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'تستطيع كأقصى حد تحميل {limit, number} {limit, plural, one{file} other{files}}.', + '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" سبق استعماله', + '{attribute} cannot be blank.' => '{attribute} لا يمكن تركه فارغًا.', + '{attribute} is invalid.' => '{attribute} غير صالح.', + '{attribute} is not a valid URL.' => '{attribute} ليس بعنوان صحيح.', + '{attribute} is not a valid email address.' => '{attribute} ليس ببريد إلكتروني صحيح.', + '{attribute} must be "{requiredValue}".' => '{attribute} يجب أن يكون "{requiredValue}".', + '{attribute} must be a number.' => '{attribute} يجب أن يكون رقمًا', + '{attribute} must be a string.' => '{attribute} يجب أن يكون كلمات', + '{attribute} must be an integer.' => '{attribute} يجب أن يكون رقمًا صحيحًا', + '{attribute} must be either "{true}" or "{false}".' => '{attribute} يجب أن يكن إما "{true}" أو "{false}".', + '{attribute} must be greater than "{compareValue}".' => '{attribute} يجب أن يكون أكبر من "{compareValue}".', + '{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} يجب أن يكون أكبر من أو يساوي "{compareValue}".', + '{attribute} must be less than "{compareValue}".' => '{attribute} يجب أن يكون أصغر من "{compareValue}".', + '{attribute} must be less than or equal to "{compareValue}".' => '{attribute} يجب أن يكون أصغر من أو يساوي "{compareValue}".', + '{attribute} must be no greater than {max}.' => '{attribute} يجب أن لا يكون أكبر من "{compareValue}".', + '{attribute} must be no less than {min}.' => '{attribute} يجب أن لا يكون أصغر من "{compareValue}".', + '{attribute} must be repeated exactly.' => '{attribute} يجب أن يكون متطابق.', + '{attribute} must not be equal to "{compareValue}".' => '{attribute} يجب أن يساوي "{compareValue}"', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} يجب أن يحتوي على أكثر من {min, number} {min, plural, one{character} other{characters}}.', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} يجب أن لا يحتوي على أكثر من {max, number} {max, plural, one{character} other{characters}}.', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} يجب أن يحتوي على {length, number} {length, plural, one{character} other{characters}}.', ); diff --git a/framework/messages/config.php b/framework/messages/config.php index 27d7bf1fd84..33497a71aca 100644 --- a/framework/messages/config.php +++ b/framework/messages/config.php @@ -1,51 +1,51 @@ __DIR__ . '/..', - // string, required, root directory containing message translations. - 'messagePath' => __DIR__, - // array, required, list of language codes that the extracted messages - // should be translated to. For example, ['zh-CN', 'de']. - 'languages' => ['ar', 'da', 'de', 'el', 'es', 'fa-IR', 'fr', 'it', 'ja', 'kz', 'lv', 'pl', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sr', 'sr-Latn', 'uk', 'zh-CN'], - // string, the name of the function for translating messages. - // Defaults to 'Yii::t'. This is used as a mark to find the messages to be - // translated. You may use a string for single function name or an array for - // multiple function names. - 'translator' => 'Yii::t', - // boolean, whether to sort messages by keys when merging new messages - // with the existing ones. Defaults to false, which means the new (untranslated) - // messages will be separated from the old (translated) ones. - 'sort' => false, - // boolean, whether the message file should be overwritten with the merged messages - 'overwrite' => true, - // boolean, whether to remove messages that no longer appear in the source code. - // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. - 'removeUnused' => false, - // array, list of patterns that specify which files/directories should NOT be processed. - // If empty or not set, all files/directories will be processed. - // A path matches a pattern if it contains the pattern string at its end. For example, - // '/a/b' will match all files and directories ending with '/a/b'; - // the '*.svn' will match all files and directories whose name ends with '.svn'. - // and the '.svn' will match all files and directories named exactly '.svn'. - // Note, the '/' characters in a pattern matches both '/' and '\'. - // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. - 'except' => [ - '.svn', - '.git', - '.gitignore', - '.gitkeep', - '.hgignore', - '.hgkeep', - '/messages', - ], - // array, list of patterns that specify which files (not directories) should be processed. - // If empty or not set, all files will be processed. - // Please refer to "except" for details about the patterns. - // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. - 'only' => ['*.php'], - // Generated file format. Can be "php", "db" or "po". - 'format' => 'php', - // Connection component ID for "db" format. - //'db' => 'db', + // string, required, root directory of all source files + 'sourcePath' => __DIR__ . '/..', + // string, required, root directory containing message translations. + 'messagePath' => __DIR__, + // array, required, list of language codes that the extracted messages + // should be translated to. For example, ['zh-CN', 'de']. + 'languages' => ['ar', 'da', 'de', 'el', 'es', 'fa-IR', 'fr', 'it', 'ja', 'kz', 'lv', 'pl', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sr', 'sr-Latn', 'uk', 'zh-CN'], + // string, the name of the function for translating messages. + // Defaults to 'Yii::t'. This is used as a mark to find the messages to be + // translated. You may use a string for single function name or an array for + // multiple function names. + 'translator' => 'Yii::t', + // boolean, whether to sort messages by keys when merging new messages + // with the existing ones. Defaults to false, which means the new (untranslated) + // messages will be separated from the old (translated) ones. + 'sort' => false, + // boolean, whether the message file should be overwritten with the merged messages + 'overwrite' => true, + // boolean, whether to remove messages that no longer appear in the source code. + // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. + 'removeUnused' => false, + // array, list of patterns that specify which files/directories should NOT be processed. + // If empty or not set, all files/directories will be processed. + // A path matches a pattern if it contains the pattern string at its end. For example, + // '/a/b' will match all files and directories ending with '/a/b'; + // the '*.svn' will match all files and directories whose name ends with '.svn'. + // and the '.svn' will match all files and directories named exactly '.svn'. + // Note, the '/' characters in a pattern matches both '/' and '\'. + // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. + 'except' => [ + '.svn', + '.git', + '.gitignore', + '.gitkeep', + '.hgignore', + '.hgkeep', + '/messages', + ], + // array, list of patterns that specify which files (not directories) should be processed. + // If empty or not set, all files will be processed. + // Please refer to "except" for details about the patterns. + // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. + 'only' => ['*.php'], + // Generated file format. Can be "php", "db" or "po". + 'format' => 'php', + // Connection component ID for "db" format. + //'db' => 'db', ]; diff --git a/framework/messages/fa-IR/yii.php b/framework/messages/fa-IR/yii.php index 1344ffcf851..cef579524b6 100644 --- a/framework/messages/fa-IR/yii.php +++ b/framework/messages/fa-IR/yii.php @@ -80,4 +80,4 @@ '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} حداقل باید شامل {min, number} کارکتر باشد.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} حداکثر باید شامل {max, number} کارکتر باشد.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} باید شامل {length, number} کارکتر باشد.', -); \ No newline at end of file +); diff --git a/framework/messages/ro/yii.php b/framework/messages/ro/yii.php index cd1017ae12f..8ed215b9d19 100644 --- a/framework/messages/ro/yii.php +++ b/framework/messages/ro/yii.php @@ -78,4 +78,4 @@ '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '«{attribute}» trebuie să conțină minim {min, number} {min, plural, one{caracter} other{caractere}}.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '«{attribute}» trebuie să conțină maxim {max, number} {max, plural, one{caracter} other{caractere}}.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '«{attribute}» trebuie să conțină {length, number} {length, plural, one{caracter} other{caractere}}.', -); \ No newline at end of file +); diff --git a/framework/messages/uk/yii.php b/framework/messages/uk/yii.php index c9c0ed0830d..4fc7e1a77a5 100644 --- a/framework/messages/uk/yii.php +++ b/framework/messages/uk/yii.php @@ -17,65 +17,65 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return array ( - 'View' => 'Перегляд', - '(not set)' => '(не задано)', - 'An internal server error occurred.' => 'Виникла внутрішня помилка сервера.', - 'Are you sure to delete this item?' => 'Ви впевнені, що хочете видалити цей елемент?', - 'Delete' => 'Видалити', - 'Error' => 'Помилка', - 'File upload failed.' => 'Завантаження файлу не вдалося.', - 'Home' => 'Головна', - 'Invalid data received for parameter "{param}".' => 'Невірне значення параметра "{param}".', - 'Login Required' => 'Потрібен вхід.', - 'Missing required arguments: {params}' => 'Відсутні обовʼязкові аргументи: {params}', - 'Missing required parameters: {params}' => 'Відсутні обовʼязкові параметри: {params}', - 'No' => 'Ні', - 'No help for unknown command "{command}".' => 'Довідка недоступна для невідомої команди "{command}".', - 'No help for unknown sub-command "{command}".' => 'Довідка недоступна для невідомої субкоманди "{command}".', - 'No results found.' => 'Нічого не знайдено.', - 'Only files with these extensions are allowed: {extensions}.' => 'Дозволене завантаження файлів тільки з наступними розширеннями: {extensions}.', - 'Only files with these mimeTypes are allowed: {mimeTypes}.' => 'Дозволене завантаження файлів тільки з наступними MIME-типами: {mimeTypes}.', - 'Page not found.' => 'Сторінка не знайдена.', - 'Please fix the following errors:' => 'Виправте такі помилки:', - 'Please upload a file.' => 'Завантажте файл.', - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'Показані записи {begin, number}-{end, number} із {totalCount, number}.', - 'The file "{file}" is not an image.' => 'Файл «{file}» не є зображенням.', - 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Файл «{file}» занадто великий. Розмір не повинен перевищувати {limit, number} {limit, plural, one{байт} few{байта} many{байт} other{байта}}.', - 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Файл «{file}» занадто маленький. Розмір повинен бути більше, ніж {limit, number} {limit, plural, one{байт} few{байта} many{байт} other{байта}}.', - 'The format of {attribute} is invalid.' => 'Невірний формат значення «{attribute}».', - 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто великий. Висота не повинна перевищувати {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', - 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто великий. Ширина не повинна перевищувати {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', - 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто маленький. Висота повинна бути більше, ніж {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', - 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто маленький. Ширина повинна бути більше, ніж {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', - 'The verification code is incorrect.' => 'Неправильний код перевірки.', - 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Всього {count, number} {count, plural, one{запис} few{записи} many{записів} other{записи}}.', - 'Unable to verify your data submission.' => 'Не вдалося перевірити передані дані.', - 'Unknown command "{command}".' => 'Невідома команда "{command}".', - 'Unknown option: --{name}' => 'Невідома опція : --{name}', - 'Update' => 'Редагувати', - 'Yes' => 'Так', - 'You are not allowed to perform this action.' => 'Вам не дозволено проводити дану дію.', - 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Ви не можете завантажувати більше {limit, number} {limit, plural, one{файла} few{файлів} many{файлів} other{файла}}.', - 'the input value' => 'введене значення', - '{attribute} "{value}" has already been taken.' => '{attribute} «{value}» вже зайнятий.', - '{attribute} cannot be blank.' => 'Необхідно заповнити «{attribute}».', - '{attribute} is invalid.' => 'Значення «{attribute}» не вірне.', - '{attribute} is not a valid URL.' => 'Значення «{attribute}» не є правильним URL.', - '{attribute} is not a valid email address.' => 'Значення «{attribute}» не є правильною email адресою.', - '{attribute} must be "{requiredValue}".' => 'Значення «{attribute}» має бути рівним «{requiredValue}».', - '{attribute} must be a number.' => 'Значення «{attribute}» має бути числом.', - '{attribute} must be a string.' => 'Значення «{attribute}» має бути рядком.', - '{attribute} must be an integer.' => 'Значення «{attribute}» має бути цілим числом.', - '{attribute} must be either "{true}" or "{false}".' => 'Значення «{attribute}» має дорівнювати «{true}» або «{false}».', - '{attribute} must be greater than "{compareValue}".' => 'Значення «{attribute}» повинно бути більшим значення «{compareValue}».', - '{attribute} must be greater than or equal to "{compareValue}".' => 'Значення «{attribute}» повинно бути більшим або дорівнювати значенню «{compareValue}».', - '{attribute} must be less than "{compareValue}".' => 'Значення «{attribute}» повинно бути меншим значення «{compareValue}».', - '{attribute} must be less than or equal to "{compareValue}".' => 'Значення «{attribute}» повинно бути меншим або дорівнювати значенню «{compareValue}».', - '{attribute} must be no greater than {max}.' => 'Значення «{attribute}» не повинно перевищувати {max}.', - '{attribute} must be no less than {min}.' => 'Значення «{attribute}» має бути більшим {min}.', - '{attribute} must be repeated exactly.' => 'Значення «{attribute}» має бути повторене в точності.', - '{attribute} must not be equal to "{compareValue}".' => 'Значення «{attribute}» не повинно бути рівним «{compareValue}».', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => 'Значення «{attribute}» повинно містити мінімум {min, number} {min, plural, one{символ} few{символа} many{символів} other{символа}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => 'Значення «{attribute}» повинно містити максимум {max, number} {max, plural, one{символ} few{символа} many{символів} other{символа}}.', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => 'Значення «{attribute}» повинно містити {length, number} {length, plural, one{символ} few{символа} many{символів} other{символа}}.', + 'View' => 'Перегляд', + '(not set)' => '(не задано)', + 'An internal server error occurred.' => 'Виникла внутрішня помилка сервера.', + 'Are you sure to delete this item?' => 'Ви впевнені, що хочете видалити цей елемент?', + 'Delete' => 'Видалити', + 'Error' => 'Помилка', + 'File upload failed.' => 'Завантаження файлу не вдалося.', + 'Home' => 'Головна', + 'Invalid data received for parameter "{param}".' => 'Невірне значення параметра "{param}".', + 'Login Required' => 'Потрібен вхід.', + 'Missing required arguments: {params}' => 'Відсутні обовʼязкові аргументи: {params}', + 'Missing required parameters: {params}' => 'Відсутні обовʼязкові параметри: {params}', + 'No' => 'Ні', + 'No help for unknown command "{command}".' => 'Довідка недоступна для невідомої команди "{command}".', + 'No help for unknown sub-command "{command}".' => 'Довідка недоступна для невідомої субкоманди "{command}".', + 'No results found.' => 'Нічого не знайдено.', + 'Only files with these extensions are allowed: {extensions}.' => 'Дозволене завантаження файлів тільки з наступними розширеннями: {extensions}.', + 'Only files with these mimeTypes are allowed: {mimeTypes}.' => 'Дозволене завантаження файлів тільки з наступними MIME-типами: {mimeTypes}.', + 'Page not found.' => 'Сторінка не знайдена.', + 'Please fix the following errors:' => 'Виправте такі помилки:', + 'Please upload a file.' => 'Завантажте файл.', + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'Показані записи {begin, number}-{end, number} із {totalCount, number}.', + 'The file "{file}" is not an image.' => 'Файл «{file}» не є зображенням.', + 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Файл «{file}» занадто великий. Розмір не повинен перевищувати {limit, number} {limit, plural, one{байт} few{байта} many{байт} other{байта}}.', + 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Файл «{file}» занадто маленький. Розмір повинен бути більше, ніж {limit, number} {limit, plural, one{байт} few{байта} many{байт} other{байта}}.', + 'The format of {attribute} is invalid.' => 'Невірний формат значення «{attribute}».', + 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто великий. Висота не повинна перевищувати {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', + 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто великий. Ширина не повинна перевищувати {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', + 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто маленький. Висота повинна бути більше, ніж {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', + 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Файл «{file}» занадто маленький. Ширина повинна бути більше, ніж {limit, number} {limit, plural, one{піксель} few{пікселя} many{пікселів} other{пікселя}}.', + 'The verification code is incorrect.' => 'Неправильний код перевірки.', + 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Всього {count, number} {count, plural, one{запис} few{записи} many{записів} other{записи}}.', + 'Unable to verify your data submission.' => 'Не вдалося перевірити передані дані.', + 'Unknown command "{command}".' => 'Невідома команда "{command}".', + 'Unknown option: --{name}' => 'Невідома опція : --{name}', + 'Update' => 'Редагувати', + 'Yes' => 'Так', + 'You are not allowed to perform this action.' => 'Вам не дозволено проводити дану дію.', + 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Ви не можете завантажувати більше {limit, number} {limit, plural, one{файла} few{файлів} many{файлів} other{файла}}.', + 'the input value' => 'введене значення', + '{attribute} "{value}" has already been taken.' => '{attribute} «{value}» вже зайнятий.', + '{attribute} cannot be blank.' => 'Необхідно заповнити «{attribute}».', + '{attribute} is invalid.' => 'Значення «{attribute}» не вірне.', + '{attribute} is not a valid URL.' => 'Значення «{attribute}» не є правильним URL.', + '{attribute} is not a valid email address.' => 'Значення «{attribute}» не є правильною email адресою.', + '{attribute} must be "{requiredValue}".' => 'Значення «{attribute}» має бути рівним «{requiredValue}».', + '{attribute} must be a number.' => 'Значення «{attribute}» має бути числом.', + '{attribute} must be a string.' => 'Значення «{attribute}» має бути рядком.', + '{attribute} must be an integer.' => 'Значення «{attribute}» має бути цілим числом.', + '{attribute} must be either "{true}" or "{false}".' => 'Значення «{attribute}» має дорівнювати «{true}» або «{false}».', + '{attribute} must be greater than "{compareValue}".' => 'Значення «{attribute}» повинно бути більшим значення «{compareValue}».', + '{attribute} must be greater than or equal to "{compareValue}".' => 'Значення «{attribute}» повинно бути більшим або дорівнювати значенню «{compareValue}».', + '{attribute} must be less than "{compareValue}".' => 'Значення «{attribute}» повинно бути меншим значення «{compareValue}».', + '{attribute} must be less than or equal to "{compareValue}".' => 'Значення «{attribute}» повинно бути меншим або дорівнювати значенню «{compareValue}».', + '{attribute} must be no greater than {max}.' => 'Значення «{attribute}» не повинно перевищувати {max}.', + '{attribute} must be no less than {min}.' => 'Значення «{attribute}» має бути більшим {min}.', + '{attribute} must be repeated exactly.' => 'Значення «{attribute}» має бути повторене в точності.', + '{attribute} must not be equal to "{compareValue}".' => 'Значення «{attribute}» не повинно бути рівним «{compareValue}».', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => 'Значення «{attribute}» повинно містити мінімум {min, number} {min, plural, one{символ} few{символа} many{символів} other{символа}}.', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => 'Значення «{attribute}» повинно містити максимум {max, number} {max, plural, one{символ} few{символа} many{символів} other{символа}}.', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => 'Значення «{attribute}» повинно містити {length, number} {length, plural, one{символ} few{символа} many{символів} other{символа}}.', ); diff --git a/framework/mutex/DbMutex.php b/framework/mutex/DbMutex.php index 3699c368f89..5a5fe5d119e 100644 --- a/framework/mutex/DbMutex.php +++ b/framework/mutex/DbMutex.php @@ -17,25 +17,25 @@ */ abstract class DbMutex extends Mutex { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the Mutex object is created, if you want to change this property, you should only assign - * it with a DB connection object. - */ - public $db = 'db'; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the Mutex object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; - /** - * Initializes generic database table based mutex implementation. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException('Mutex::db must be either a DB connection instance or the application component ID of a DB connection.'); - } - } + /** + * Initializes generic database table based mutex implementation. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException('Mutex::db must be either a DB connection instance or the application component ID of a DB connection.'); + } + } } diff --git a/framework/mutex/FileMutex.php b/framework/mutex/FileMutex.php index fd5cb00d2cd..2be1bc45d09 100644 --- a/framework/mutex/FileMutex.php +++ b/framework/mutex/FileMutex.php @@ -17,88 +17,90 @@ */ class FileMutex extends Mutex { - /** - * @var string the directory to store mutex files. You may use path alias here. - * Defaults to the "mutex" subdirectory under the application runtime path. - */ - public $mutexPath = '@runtime/mutex'; - /** - * @var integer the permission to be set for newly created mutex files. - * This value will be used by PHP chmod() function. No umask will be applied. - * If not set, the permission will be determined by the current environment. - */ - public $fileMode; - /** - * @var integer the permission to be set for newly created directories. - * This value will be used by PHP chmod() function. No umask will be applied. - * Defaults to 0775, meaning the directory is read-writable by owner and group, - * but read-only for other users. - */ - public $dirMode = 0775; - /** - * @var resource[] stores all opened lock files. Keys are lock names and values are file handles. - */ - private $_files = []; + /** + * @var string the directory to store mutex files. You may use path alias here. + * Defaults to the "mutex" subdirectory under the application runtime path. + */ + public $mutexPath = '@runtime/mutex'; + /** + * @var integer the permission to be set for newly created mutex files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + /** + * @var resource[] stores all opened lock files. Keys are lock names and values are file handles. + */ + private $_files = []; + /** + * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like + * operating systems. + * @throws InvalidConfigException + */ + public function init() + { + if (stripos(php_uname('s'), 'win') === 0) { + throw new InvalidConfigException('FileMutex does not have MS Windows operating system support.'); + } + $this->mutexPath = Yii::getAlias($this->mutexPath); + if (!is_dir($this->mutexPath)) { + FileHelper::createDirectory($this->mutexPath, $this->dirMode, true); + } + } - /** - * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like - * operating systems. - * @throws InvalidConfigException - */ - public function init() - { - if (stripos(php_uname('s'), 'win') === 0) { - throw new InvalidConfigException('FileMutex does not have MS Windows operating system support.'); - } - $this->mutexPath = Yii::getAlias($this->mutexPath); - if (!is_dir($this->mutexPath)) { - FileHelper::createDirectory($this->mutexPath, $this->dirMode, true); - } - } + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + protected function acquireLock($name, $timeout = 0) + { + $fileName = $this->mutexPath . '/' . md5($name) . '.lock'; + $file = fopen($fileName, 'w+'); + if ($file === false) { + return false; + } + if ($this->fileMode !== null) { + @chmod($fileName, $this->fileMode); + } + $waitTime = 0; + while (!flock($file, LOCK_EX | LOCK_NB)) { + $waitTime++; + if ($waitTime > $timeout) { + fclose($file); - /** - * Acquires lock by given name. - * @param string $name of the lock to be acquired. - * @param integer $timeout to wait for lock to become released. - * @return boolean acquiring result. - */ - protected function acquireLock($name, $timeout = 0) - { - $fileName = $this->mutexPath . '/' . md5($name) . '.lock'; - $file = fopen($fileName, 'w+'); - if ($file === false) { - return false; - } - if ($this->fileMode !== null) { - @chmod($fileName, $this->fileMode); - } - $waitTime = 0; - while (!flock($file, LOCK_EX | LOCK_NB)) { - $waitTime++; - if ($waitTime > $timeout) { - fclose($file); - return false; - } - sleep(1); - } - $this->_files[$name] = $file; - return true; - } + return false; + } + sleep(1); + } + $this->_files[$name] = $file; - /** - * Releases lock by given name. - * @param string $name of the lock to be released. - * @return boolean release result. - */ - protected function releaseLock($name) - { - if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { - return false; - } else { - fclose($this->_files[$name]); - unset($this->_files[$name]); - return true; - } - } + return true; + } + + /** + * Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + protected function releaseLock($name) + { + if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { + return false; + } else { + fclose($this->_files[$name]); + unset($this->_files[$name]); + + return true; + } + } } diff --git a/framework/mutex/Mutex.php b/framework/mutex/Mutex.php index 611e7255472..1e1dd5e69cd 100644 --- a/framework/mutex/Mutex.php +++ b/framework/mutex/Mutex.php @@ -16,80 +16,81 @@ */ abstract class Mutex extends Component { - /** - * @var boolean whether all locks acquired in this process (i.e. local locks) must be released automagically - * before finishing script execution. Defaults to true. Setting this property to true means that all locks - * acquire in this process must be released in any case (regardless any kind of errors or exceptions). - */ - public $autoRelease = true; - /** - * @var string[] names of the locks acquired in the current PHP process. - */ - private $_locks = []; + /** + * @var boolean whether all locks acquired in this process (i.e. local locks) must be released automagically + * before finishing script execution. Defaults to true. Setting this property to true means that all locks + * acquire in this process must be released in any case (regardless any kind of errors or exceptions). + */ + public $autoRelease = true; + /** + * @var string[] names of the locks acquired in the current PHP process. + */ + private $_locks = []; + /** + * Initializes the mutex component. + */ + public function init() + { + if ($this->autoRelease) { + $locks = &$this->_locks; + register_shutdown_function(function () use (&$locks) { + foreach ($locks as $lock) { + $this->release($lock); + } + }); + } + } - /** - * Initializes the mutex component. - */ - public function init() - { - if ($this->autoRelease) { - $locks = &$this->_locks; - register_shutdown_function(function () use (&$locks) { - foreach ($locks as $lock) { - $this->release($lock); - } - }); - } - } + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. Must be unique. + * @param integer $timeout to wait for lock to be released. Defaults to zero meaning that method will return + * false immediately in case lock was already acquired. + * @return boolean lock acquiring result. + */ + public function acquire($name, $timeout = 0) + { + if ($this->acquireLock($name, $timeout)) { + $this->_locks[] = $name; - /** - * Acquires lock by given name. - * @param string $name of the lock to be acquired. Must be unique. - * @param integer $timeout to wait for lock to be released. Defaults to zero meaning that method will return - * false immediately in case lock was already acquired. - * @return boolean lock acquiring result. - */ - public function acquire($name, $timeout = 0) - { - if ($this->acquireLock($name, $timeout)) { - $this->_locks[] = $name; - return true; - } else { - return false; - } - } + return true; + } else { + return false; + } + } - /** - * Release acquired lock. This method will return false in case named lock was not found. - * @param string $name of the lock to be released. This lock must be already created. - * @return boolean lock release result: false in case named lock was not found.. - */ - public function release($name) - { - if ($this->releaseLock($name)) { - $index = array_search($name, $this->_locks); - if ($index !== false) { - unset($this->_locks[$index]); - } - return true; - } else { - return false; - } - } + /** + * Release acquired lock. This method will return false in case named lock was not found. + * @param string $name of the lock to be released. This lock must be already created. + * @return boolean lock release result: false in case named lock was not found.. + */ + public function release($name) + { + if ($this->releaseLock($name)) { + $index = array_search($name, $this->_locks); + if ($index !== false) { + unset($this->_locks[$index]); + } - /** - * This method should be extended by concrete mutex implementations. Acquires lock by given name. - * @param string $name of the lock to be acquired. - * @param integer $timeout to wait for lock to become released. - * @return boolean acquiring result. - */ - abstract protected function acquireLock($name, $timeout = 0); + return true; + } else { + return false; + } + } - /** - * This method should be extended by concrete mutex implementations. Releases lock by given name. - * @param string $name of the lock to be released. - * @return boolean release result. - */ - abstract protected function releaseLock($name); + /** + * This method should be extended by concrete mutex implementations. Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + abstract protected function acquireLock($name, $timeout = 0); + + /** + * This method should be extended by concrete mutex implementations. Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + abstract protected function releaseLock($name); } diff --git a/framework/mutex/MysqlMutex.php b/framework/mutex/MysqlMutex.php index d18b601d98a..3d0c110e477 100644 --- a/framework/mutex/MysqlMutex.php +++ b/framework/mutex/MysqlMutex.php @@ -16,42 +16,42 @@ */ class MysqlMutex extends DbMutex { - /** - * Initializes MySQL specific mutex component implementation. - * @throws InvalidConfigException if [[db]] is not MySQL connection. - */ - public function init() - { - parent::init(); - if ($this->db->driverName !== 'mysql') { - throw new InvalidConfigException('In order to use MysqlMutex connection must be configured to use MySQL database.'); - } - } + /** + * Initializes MySQL specific mutex component implementation. + * @throws InvalidConfigException if [[db]] is not MySQL connection. + */ + public function init() + { + parent::init(); + if ($this->db->driverName !== 'mysql') { + throw new InvalidConfigException('In order to use MysqlMutex connection must be configured to use MySQL database.'); + } + } - /** - * Acquires lock by given name. - * @param string $name of the lock to be acquired. - * @param integer $timeout to wait for lock to become released. - * @return boolean acquiring result. - * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock - */ - protected function acquireLock($name, $timeout = 0) - { - return (boolean)$this->db - ->createCommand('SELECT GET_LOCK(:name, :timeout)', [':name' => $name, ':timeout' => $timeout]) - ->queryScalar(); - } + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock + */ + protected function acquireLock($name, $timeout = 0) + { + return (boolean) $this->db + ->createCommand('SELECT GET_LOCK(:name, :timeout)', [':name' => $name, ':timeout' => $timeout]) + ->queryScalar(); + } - /** - * Releases lock by given name. - * @param string $name of the lock to be released. - * @return boolean release result. - * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock - */ - protected function releaseLock($name) - { - return (boolean)$this->db - ->createCommand('SELECT RELEASE_LOCK(:name)', [':name' => $name]) - ->queryScalar(); - } + /** + * Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + */ + protected function releaseLock($name) + { + return (boolean) $this->db + ->createCommand('SELECT RELEASE_LOCK(:name)', [':name' => $name]) + ->queryScalar(); + } } diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php index 2348a9ab38f..7e908505b0c 100644 --- a/framework/rbac/Assignment.php +++ b/framework/rbac/Assignment.php @@ -22,34 +22,34 @@ */ class Assignment extends Object { - /** - * @var Manager the auth manager of this item - */ - public $manager; - /** - * @var string the business rule associated with this assignment - */ - public $bizRule; - /** - * @var mixed additional data for this assignment - */ - public $data; - /** - * @var mixed user ID (see [[\yii\web\User::id]]). Do not modify this property after it is populated. - * To modify the user ID of an assignment, you must remove the assignment and create a new one. - */ - public $userId; - /** - * @return string the authorization item name. Do not modify this property after it is populated. - * To modify the item name of an assignment, you must remove the assignment and create a new one. - */ - public $itemName; + /** + * @var Manager the auth manager of this item + */ + public $manager; + /** + * @var string the business rule associated with this assignment + */ + public $bizRule; + /** + * @var mixed additional data for this assignment + */ + public $data; + /** + * @var mixed user ID (see [[\yii\web\User::id]]). Do not modify this property after it is populated. + * To modify the user ID of an assignment, you must remove the assignment and create a new one. + */ + public $userId; + /** + * @return string the authorization item name. Do not modify this property after it is populated. + * To modify the item name of an assignment, you must remove the assignment and create a new one. + */ + public $itemName; - /** - * Saves the changes to an authorization assignment. - */ - public function save() - { - $this->manager->saveAssignment($this); - } + /** + * Saves the changes to an authorization assignment. + */ + public function save() + { + $this->manager->saveAssignment($this); + } } diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index dd1c5cef5a4..b1236e0c4d6 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -32,567 +32,581 @@ */ class DbManager extends Manager { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbManager object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; - /** - * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. - */ - public $itemTable = '{{%auth_item}}'; - /** - * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. - */ - public $itemChildTable = '{{%auth_item_child}}'; - /** - * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. - */ - public $assignmentTable = '{{%auth_assignment}}'; - - private $_usingSqlite; - - /** - * Initializes the application component. - * This method overrides the parent implementation by establishing the database connection. - */ - public function init() - { - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); - } - $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); - parent::init(); - } - - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This should can be either an integer or a string representing - * the unique identifier of a user. See [[\yii\web\User::id]]. - * @param string $itemName the name of the operation that need access check - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, - * which holds the value of `$userId`. - * @return boolean whether the operations can be performed by the user. - */ - public function checkAccess($userId, $itemName, $params = []) - { - $assignments = $this->getAssignments($userId); - return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); - } - - /** - * Performs access check for the specified user. - * This method is internally called by [[checkAccess()]]. - * @param mixed $userId the user ID. This should can be either an integer or a string representing - * the unique identifier of a user. See [[\yii\web\User::id]]. - * @param string $itemName the name of the operation that need access check - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, - * which holds the value of `$userId`. - * @param Assignment[] $assignments the assignments to the specified user - * @return boolean whether the operations can be performed by the user. - */ - protected function checkAccessRecursive($userId, $itemName, $params, $assignments) - { - if (($item = $this->getItem($itemName)) === null) { - return false; - } - Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); - if (!isset($params['userId'])) { - $params['userId'] = $userId; - } - if ($this->executeBizRule($item->bizRule, $params, $item->data)) { - if (in_array($itemName, $this->defaultRoles)) { - return true; - } - if (isset($assignments[$itemName])) { - $assignment = $assignments[$itemName]; - if ($this->executeBizRule($assignment->bizRule, $params, $assignment->data)) { - return true; - } - } - $query = new Query; - $parents = $query->select(['parent']) - ->from($this->itemChildTable) - ->where(['child' => $itemName]) - ->createCommand($this->db) - ->queryColumn(); - foreach ($parents as $parent) { - if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { - return true; - } - } - } - return false; - } - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the item is added successfully - * @throws Exception if either parent or child doesn't exist. - * @throws InvalidCallException if a loop has been detected. - */ - public function addItemChild($itemName, $childName) - { - if ($itemName === $childName) { - throw new Exception("Cannot add '$itemName' as a child of itself."); - } - $query = new Query; - $rows = $query->from($this->itemTable) - ->where(['or', 'name=:name1', 'name=:name2'], [':name1' => $itemName, ':name2' => $childName]) - ->createCommand($this->db) - ->queryAll(); - if (count($rows) == 2) { - if ($rows[0]['name'] === $itemName) { - $parentType = $rows[0]['type']; - $childType = $rows[1]['type']; - } else { - $childType = $rows[0]['type']; - $parentType = $rows[1]['type']; - } - $this->checkItemChildType($parentType, $childType); - if ($this->detectLoop($itemName, $childName)) { - throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); - } - $this->db->createCommand() - ->insert($this->itemChildTable, ['parent' => $itemName, 'child' => $childName]) - ->execute(); - return true; - } else { - throw new Exception("Either '$itemName' or '$childName' does not exist."); - } - } - - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - public function removeItemChild($itemName, $childName) - { - return $this->db->createCommand() - ->delete($this->itemChildTable, ['parent' => $itemName, 'child' => $childName]) - ->execute() > 0; - } - - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - public function hasItemChild($itemName, $childName) - { - $query = new Query; - return $query->select(['parent']) - ->from($this->itemChildTable) - ->where(['parent' => $itemName, 'child' => $childName]) - ->createCommand($this->db) - ->queryScalar() !== false; - } - - /** - * Returns the children of the specified item. - * @param mixed $names the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - public function getItemChildren($names) - { - $query = new Query; - $rows = $query->select(['name', 'type', 'description', 'biz_rule', 'data']) - ->from([$this->itemTable, $this->itemChildTable]) - ->where(['parent' => $names, 'name' => new Expression('child')]) - ->createCommand($this->db) - ->queryAll(); - $children = []; - foreach ($rows as $row) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - $children[$row['name']] = new Item([ - 'manager' => $this, - 'name' => $row['name'], - 'type' => $row['type'], - 'description' => $row['description'], - 'bizRule' => $row['biz_rule'], - 'data' => $data, - ]); - } - return $children; - } - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user - */ - public function assign($userId, $itemName, $bizRule = null, $data = null) - { - if ($this->usingSqlite() && $this->getItem($itemName) === null) { - throw new InvalidParamException("The item '$itemName' does not exist."); - } - $this->db->createCommand() - ->insert($this->assignmentTable, [ - 'user_id' => $userId, - 'item_name' => $itemName, - 'biz_rule' => $bizRule, - 'data' => $data === null ? null : serialize($data), - ]) - ->execute(); - return new Assignment([ - 'manager' => $this, - 'userId' => $userId, - 'itemName' => $itemName, - 'bizRule' => $bizRule, - 'data' => $data, - ]); - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - public function revoke($userId, $itemName) - { - return $this->db->createCommand() - ->delete($this->assignmentTable, ['user_id' => $userId, 'item_name' => $itemName]) - ->execute() > 0; - } - - /** - * Revokes all authorization assignments from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - */ - public function revokeAll($userId) - { - return $this->db->createCommand() - ->delete($this->assignmentTable, ['user_id' => $userId]) - ->execute() > 0; - } - - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - public function isAssigned($userId, $itemName) - { - $query = new Query; - return $query->select(['item_name']) - ->from($this->assignmentTable) - ->where(['user_id' => $userId, 'item_name' => $itemName]) - ->createCommand($this->db) - ->queryScalar() !== false; - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - public function getAssignment($userId, $itemName) - { - $query = new Query; - $row = $query->from($this->assignmentTable) - ->where(['user_id' => $userId, 'item_name' => $itemName]) - ->createCommand($this->db) - ->queryOne(); - if ($row !== false) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - return new Assignment([ - 'manager' => $this, - 'userId' => $row['user_id'], - 'itemName' => $row['item_name'], - 'bizRule' => $row['biz_rule'], - 'data' => $data, - ]); - } else { - return null; - } - } - - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return Assignment[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - public function getAssignments($userId) - { - $query = new Query; - $rows = $query->from($this->assignmentTable) - ->where(['user_id' => $userId]) - ->createCommand($this->db) - ->queryAll(); - $assignments = []; - foreach ($rows as $row) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - $assignments[$row['item_name']] = new Assignment([ - 'manager' => $this, - 'userId' => $row['user_id'], - 'itemName' => $row['item_name'], - 'bizRule' => $row['biz_rule'], - 'data' => $data, - ]); - } - return $assignments; - } - - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - public function saveAssignment($assignment) - { - $this->db->createCommand() - ->update($this->assignmentTable, [ - 'biz_rule' => $assignment->bizRule, - 'data' => $assignment->data === null ? null : serialize($assignment->data), - ], [ - 'user_id' => $assignment->userId, - 'item_name' => $assignment->itemName, - ]) - ->execute(); - } - - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - public function getItems($userId = null, $type = null) - { - $query = new Query; - if ($userId === null && $type === null) { - $command = $query->from($this->itemTable) - ->createCommand($this->db); - } elseif ($userId === null) { - $command = $query->from($this->itemTable) - ->where(['type' => $type]) - ->createCommand($this->db); - } elseif ($type === null) { - $command = $query->select(['name', 'type', 'description', 't1.biz_rule', 't1.data']) - ->from([$this->itemTable . ' t1', $this->assignmentTable . ' t2']) - ->where(['user_id' => $userId, 'name' => new Expression('item_name')]) - ->createCommand($this->db); - } else { - $command = $query->select(['name', 'type', 'description', 't1.biz_rule', 't1.data']) - ->from([$this->itemTable . ' t1', $this->assignmentTable . ' t2']) - ->where(['user_id' => $userId, 'type' => $type, 'name' => new Expression('item_name')]) - ->createCommand($this->db); - } - $items = []; - foreach ($command->queryAll() as $row) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - $items[$row['name']] = new Item([ - 'manager' => $this, - 'name' => $row['name'], - 'type' => $row['type'], - 'description' => $row['description'], - 'bizRule' => $row['biz_rule'], - 'data' => $data, - ]); - } - return $items; - } - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @return Item the authorization item - * @throws Exception if an item with the same name already exists - */ - public function createItem($name, $type, $description = '', $bizRule = null, $data = null) - { - $this->db->createCommand() - ->insert($this->itemTable, [ - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'biz_rule' => $bizRule, - 'data' => $data === null ? null : serialize($data), - ]) - ->execute(); - return new Item([ - 'manager' => $this, - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'bizRule' => $bizRule, - 'data' => $data, - ]); - } - - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - public function removeItem($name) - { - if ($this->usingSqlite()) { - $this->db->createCommand() - ->delete($this->itemChildTable, ['or', 'parent=:name', 'child=:name'], [':name' => $name]) - ->execute(); - $this->db->createCommand() - ->delete($this->assignmentTable, ['item_name' => $name]) - ->execute(); - } - return $this->db->createCommand() - ->delete($this->itemTable, ['name' => $name]) - ->execute() > 0; - } - - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - public function getItem($name) - { - $query = new Query; - $row = $query->from($this->itemTable) - ->where(['name' => $name]) - ->createCommand($this->db) - ->queryOne(); - - if ($row !== false) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - return new Item([ - 'manager' => $this, - 'name' => $row['name'], - 'type' => $row['type'], - 'description' => $row['description'], - 'bizRule' => $row['biz_rule'], - 'data' => $data, - ]); - } else { - return null; - } - } - - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - */ - public function saveItem($item, $oldName = null) - { - if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { - $this->db->createCommand() - ->update($this->itemChildTable, ['parent' => $item->getName()], ['parent' => $oldName]) - ->execute(); - $this->db->createCommand() - ->update($this->itemChildTable, ['child' => $item->getName()], ['child' => $oldName]) - ->execute(); - $this->db->createCommand() - ->update($this->assignmentTable, ['item_name' => $item->getName()], ['item_name' => $oldName]) - ->execute(); - } - - $this->db->createCommand() - ->update($this->itemTable, [ - 'name' => $item->getName(), - 'type' => $item->type, - 'description' => $item->description, - 'biz_rule' => $item->bizRule, - 'data' => $item->data === null ? null : serialize($item->data), - ], [ - 'name' => $oldName === null ? $item->getName() : $oldName, - ]) - ->execute(); - } - - /** - * Saves the authorization data to persistent storage. - */ - public function save() - { - } - - /** - * Removes all authorization data. - */ - public function clearAll() - { - $this->clearAssignments(); - $this->db->createCommand()->delete($this->itemChildTable)->execute(); - $this->db->createCommand()->delete($this->itemTable)->execute(); - } - - /** - * Removes all authorization assignments. - */ - public function clearAssignments() - { - $this->db->createCommand()->delete($this->assignmentTable)->execute(); - } - - /** - * Checks whether there is a loop in the authorization item hierarchy. - * @param string $itemName parent item name - * @param string $childName the name of the child item that is to be added to the hierarchy - * @return boolean whether a loop exists - */ - protected function detectLoop($itemName, $childName) - { - if ($childName === $itemName) { - return true; - } - foreach ($this->getItemChildren($childName) as $child) { - if ($this->detectLoop($itemName, $child->getName())) { - return true; - } - } - return false; - } - - /** - * @return boolean whether the database is a SQLite database - */ - protected function usingSqlite() - { - return $this->_usingSqlite; - } + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbManager object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. + */ + public $itemTable = '{{%auth_item}}'; + /** + * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. + */ + public $itemChildTable = '{{%auth_item_child}}'; + /** + * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. + */ + public $assignmentTable = '{{%auth_assignment}}'; + + private $_usingSqlite; + + /** + * Initializes the application component. + * This method overrides the parent implementation by establishing the database connection. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); + } + $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); + parent::init(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[\yii\web\User::id]]. + * @param string $itemName the name of the operation that need access check + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = []) + { + $assignments = $this->getAssignments($userId); + + return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); + } + + /** + * Performs access check for the specified user. + * This method is internally called by [[checkAccess()]]. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[\yii\web\User::id]]. + * @param string $itemName the name of the operation that need access check + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @param Assignment[] $assignments the assignments to the specified user + * @return boolean whether the operations can be performed by the user. + */ + protected function checkAccessRecursive($userId, $itemName, $params, $assignments) + { + if (($item = $this->getItem($itemName)) === null) { + return false; + } + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->bizRule, $params, $item->data)) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($assignments[$itemName])) { + $assignment = $assignments[$itemName]; + if ($this->executeBizRule($assignment->bizRule, $params, $assignment->data)) { + return true; + } + } + $query = new Query; + $parents = $query->select(['parent']) + ->from($this->itemChildTable) + ->where(['child' => $itemName]) + ->createCommand($this->db) + ->queryColumn(); + foreach ($parents as $parent) { + if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { + return true; + } + } + } + + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if ($itemName === $childName) { + throw new Exception("Cannot add '$itemName' as a child of itself."); + } + $query = new Query; + $rows = $query->from($this->itemTable) + ->where(['or', 'name=:name1', 'name=:name2'], [':name1' => $itemName, ':name2' => $childName]) + ->createCommand($this->db) + ->queryAll(); + if (count($rows) == 2) { + if ($rows[0]['name'] === $itemName) { + $parentType = $rows[0]['type']; + $childType = $rows[1]['type']; + } else { + $childType = $rows[0]['type']; + $parentType = $rows[1]['type']; + } + $this->checkItemChildType($parentType, $childType); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + $this->db->createCommand() + ->insert($this->itemChildTable, ['parent' => $itemName, 'child' => $childName]) + ->execute(); + + return true; + } else { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + return $this->db->createCommand() + ->delete($this->itemChildTable, ['parent' => $itemName, 'child' => $childName]) + ->execute() > 0; + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + $query = new Query; + + return $query->select(['parent']) + ->from($this->itemChildTable) + ->where(['parent' => $itemName, 'child' => $childName]) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + $query = new Query; + $rows = $query->select(['name', 'type', 'description', 'biz_rule', 'data']) + ->from([$this->itemTable, $this->itemChildTable]) + ->where(['parent' => $names, 'name' => new Expression('child')]) + ->createCommand($this->db) + ->queryAll(); + $children = []; + foreach ($rows as $row) { + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; + } + $children[$row['name']] = new Item([ + 'manager' => $this, + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + ]); + } + + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if ($this->usingSqlite() && $this->getItem($itemName) === null) { + throw new InvalidParamException("The item '$itemName' does not exist."); + } + $this->db->createCommand() + ->insert($this->assignmentTable, [ + 'user_id' => $userId, + 'item_name' => $itemName, + 'biz_rule' => $bizRule, + 'data' => $data === null ? null : serialize($data), + ]) + ->execute(); + + return new Assignment([ + 'manager' => $this, + 'userId' => $userId, + 'itemName' => $itemName, + 'bizRule' => $bizRule, + 'data' => $data, + ]); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, ['user_id' => $userId, 'item_name' => $itemName]) + ->execute() > 0; + } + + /** + * Revokes all authorization assignments from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether removal is successful + */ + public function revokeAll($userId) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, ['user_id' => $userId]) + ->execute() > 0; + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName) + { + $query = new Query; + + return $query->select(['item_name']) + ->from($this->assignmentTable) + ->where(['user_id' => $userId, 'item_name' => $itemName]) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + $query = new Query; + $row = $query->from($this->assignmentTable) + ->where(['user_id' => $userId, 'item_name' => $itemName]) + ->createCommand($this->db) + ->queryOne(); + if ($row !== false) { + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; + } + + return new Assignment([ + 'manager' => $this, + 'userId' => $row['user_id'], + 'itemName' => $row['item_name'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + ]); + } else { + return null; + } + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + $query = new Query; + $rows = $query->from($this->assignmentTable) + ->where(['user_id' => $userId]) + ->createCommand($this->db) + ->queryAll(); + $assignments = []; + foreach ($rows as $row) { + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; + } + $assignments[$row['item_name']] = new Assignment([ + 'manager' => $this, + 'userId' => $row['user_id'], + 'itemName' => $row['item_name'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + ]); + } + + return $assignments; + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + $this->db->createCommand() + ->update($this->assignmentTable, [ + 'biz_rule' => $assignment->bizRule, + 'data' => $assignment->data === null ? null : serialize($assignment->data), + ], [ + 'user_id' => $assignment->userId, + 'item_name' => $assignment->itemName, + ]) + ->execute(); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + $query = new Query; + if ($userId === null && $type === null) { + $command = $query->from($this->itemTable) + ->createCommand($this->db); + } elseif ($userId === null) { + $command = $query->from($this->itemTable) + ->where(['type' => $type]) + ->createCommand($this->db); + } elseif ($type === null) { + $command = $query->select(['name', 'type', 'description', 't1.biz_rule', 't1.data']) + ->from([$this->itemTable . ' t1', $this->assignmentTable . ' t2']) + ->where(['user_id' => $userId, 'name' => new Expression('item_name')]) + ->createCommand($this->db); + } else { + $command = $query->select(['name', 'type', 'description', 't1.biz_rule', 't1.data']) + ->from([$this->itemTable . ' t1', $this->assignmentTable . ' t2']) + ->where(['user_id' => $userId, 'type' => $type, 'name' => new Expression('item_name')]) + ->createCommand($this->db); + } + $items = []; + foreach ($command->queryAll() as $row) { + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; + } + $items[$row['name']] = new Item([ + 'manager' => $this, + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + ]); + } + + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + $this->db->createCommand() + ->insert($this->itemTable, [ + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'biz_rule' => $bizRule, + 'data' => $data === null ? null : serialize($data), + ]) + ->execute(); + + return new Item([ + 'manager' => $this, + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizRule' => $bizRule, + 'data' => $data, + ]); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if ($this->usingSqlite()) { + $this->db->createCommand() + ->delete($this->itemChildTable, ['or', 'parent=:name', 'child=:name'], [':name' => $name]) + ->execute(); + $this->db->createCommand() + ->delete($this->assignmentTable, ['item_name' => $name]) + ->execute(); + } + + return $this->db->createCommand() + ->delete($this->itemTable, ['name' => $name]) + ->execute() > 0; + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + $query = new Query; + $row = $query->from($this->itemTable) + ->where(['name' => $name]) + ->createCommand($this->db) + ->queryOne(); + + if ($row !== false) { + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; + } + + return new Item([ + 'manager' => $this, + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'bizRule' => $row['biz_rule'], + 'data' => $data, + ]); + } else { + return null; + } + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveItem($item, $oldName = null) + { + if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { + $this->db->createCommand() + ->update($this->itemChildTable, ['parent' => $item->getName()], ['parent' => $oldName]) + ->execute(); + $this->db->createCommand() + ->update($this->itemChildTable, ['child' => $item->getName()], ['child' => $oldName]) + ->execute(); + $this->db->createCommand() + ->update($this->assignmentTable, ['item_name' => $item->getName()], ['item_name' => $oldName]) + ->execute(); + } + + $this->db->createCommand() + ->update($this->itemTable, [ + 'name' => $item->getName(), + 'type' => $item->type, + 'description' => $item->description, + 'biz_rule' => $item->bizRule, + 'data' => $item->data === null ? null : serialize($item->data), + ], [ + 'name' => $oldName === null ? $item->getName() : $oldName, + ]) + ->execute(); + } + + /** + * Saves the authorization data to persistent storage. + */ + public function save() + { + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->db->createCommand()->delete($this->itemChildTable)->execute(); + $this->db->createCommand()->delete($this->itemTable)->execute(); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->db->createCommand()->delete($this->assignmentTable)->execute(); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + foreach ($this->getItemChildren($childName) as $child) { + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + + return false; + } + + /** + * @return boolean whether the database is a SQLite database + */ + protected function usingSqlite() + { + return $this->_usingSqlite; + } } diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php index 135fe4fb334..028658b6fcf 100644 --- a/framework/rbac/Item.php +++ b/framework/rbac/Item.php @@ -27,178 +27,178 @@ */ class Item extends Object { - const TYPE_OPERATION = 0; - const TYPE_TASK = 1; - const TYPE_ROLE = 2; - - /** - * @var Manager the auth manager of this item - */ - public $manager; - /** - * @var string the item description - */ - public $description; - /** - * @var string the business rule associated with this item - */ - public $bizRule; - /** - * @var mixed the additional data associated with this item - */ - public $data; - /** - * @var integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). - */ - public $type; - - private $_name; - private $_oldName; - - - /** - * Checks to see if the specified item is within the hierarchy starting from this item. - * This method is expected to be internally used by the actual implementations - * of the [[Manager::checkAccess()]]. - * @param string $itemName the name of the item to be checked - * @param array $params the parameters to be passed to business rule evaluation - * @return boolean whether the specified item is within the hierarchy starting from this item. - */ - public function checkAccess($itemName, $params = []) - { - Yii::trace('Checking permission: ' . $this->_name, __METHOD__); - if ($this->manager->executeBizRule($this->bizRule, $params, $this->data)) { - if ($this->_name == $itemName) { - return true; - } - foreach ($this->manager->getItemChildren($this->_name) as $item) { - if ($item->checkAccess($itemName, $params)) { - return true; - } - } - } - return false; - } - - /** - * @return string the item name - */ - public function getName() - { - return $this->_name; - } - - /** - * @param string $value the item name - */ - public function setName($value) - { - if ($this->_name !== $value) { - $this->_oldName = $this->_name; - $this->_name = $value; - } - } - - /** - * Adds a child item. - * @param string $name the name of the child item - * @return boolean whether the item is added successfully - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - * @see Manager::addItemChild - */ - public function addChild($name) - { - return $this->manager->addItemChild($this->_name, $name); - } - - /** - * Removes a child item. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $name the child item name - * @return boolean whether the removal is successful - * @see Manager::removeItemChild - */ - public function removeChild($name) - { - return $this->manager->removeItemChild($this->_name, $name); - } - - /** - * Returns a value indicating whether a child exists - * @param string $name the child item name - * @return boolean whether the child exists - * @see Manager::hasItemChild - */ - public function hasChild($name) - { - return $this->manager->hasItemChild($this->_name, $name); - } - - /** - * Returns the children of this item. - * @return Item[] all child items of this item. - * @see Manager::getItemChildren - */ - public function getChildren() - { - return $this->manager->getItemChildren($this->_name); - } - - /** - * Assigns this item to a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws \yii\base\Exception if the item has already been assigned to the user - * @see Manager::assign - */ - public function assign($userId, $bizRule = null, $data = null) - { - return $this->manager->assign($userId, $this->_name, $bizRule, $data); - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - * @see Manager::revoke - */ - public function revoke($userId) - { - return $this->manager->revoke($userId, $this->_name); - } - - /** - * Returns a value indicating whether this item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether the item has been assigned to the user. - * @see Manager::isAssigned - */ - public function isAssigned($userId) - { - return $this->manager->isAssigned($userId, $this->_name); - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return Assignment the item assignment information. Null is returned if - * this item is not assigned to the user. - * @see Manager::getAssignment - */ - public function getAssignment($userId) - { - return $this->manager->getAssignment($userId, $this->_name); - } - - /** - * Saves an authorization item to persistent storage. - */ - public function save() - { - $this->manager->saveItem($this, $this->_oldName); - $this->_oldName = null; - } + const TYPE_OPERATION = 0; + const TYPE_TASK = 1; + const TYPE_ROLE = 2; + + /** + * @var Manager the auth manager of this item + */ + public $manager; + /** + * @var string the item description + */ + public $description; + /** + * @var string the business rule associated with this item + */ + public $bizRule; + /** + * @var mixed the additional data associated with this item + */ + public $data; + /** + * @var integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + */ + public $type; + + private $_name; + private $_oldName; + + /** + * Checks to see if the specified item is within the hierarchy starting from this item. + * This method is expected to be internally used by the actual implementations + * of the [[Manager::checkAccess()]]. + * @param string $itemName the name of the item to be checked + * @param array $params the parameters to be passed to business rule evaluation + * @return boolean whether the specified item is within the hierarchy starting from this item. + */ + public function checkAccess($itemName, $params = []) + { + Yii::trace('Checking permission: ' . $this->_name, __METHOD__); + if ($this->manager->executeBizRule($this->bizRule, $params, $this->data)) { + if ($this->_name == $itemName) { + return true; + } + foreach ($this->manager->getItemChildren($this->_name) as $item) { + if ($item->checkAccess($itemName, $params)) { + return true; + } + } + } + + return false; + } + + /** + * @return string the item name + */ + public function getName() + { + return $this->_name; + } + + /** + * @param string $value the item name + */ + public function setName($value) + { + if ($this->_name !== $value) { + $this->_oldName = $this->_name; + $this->_name = $value; + } + } + + /** + * Adds a child item. + * @param string $name the name of the child item + * @return boolean whether the item is added successfully + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + * @see Manager::addItemChild + */ + public function addChild($name) + { + return $this->manager->addItemChild($this->_name, $name); + } + + /** + * Removes a child item. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $name the child item name + * @return boolean whether the removal is successful + * @see Manager::removeItemChild + */ + public function removeChild($name) + { + return $this->manager->removeItemChild($this->_name, $name); + } + + /** + * Returns a value indicating whether a child exists + * @param string $name the child item name + * @return boolean whether the child exists + * @see Manager::hasItemChild + */ + public function hasChild($name) + { + return $this->manager->hasItemChild($this->_name, $name); + } + + /** + * Returns the children of this item. + * @return Item[] all child items of this item. + * @see Manager::getItemChildren + */ + public function getChildren() + { + return $this->manager->getItemChildren($this->_name); + } + + /** + * Assigns this item to a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item has already been assigned to the user + * @see Manager::assign + */ + public function assign($userId, $bizRule = null, $data = null) + { + return $this->manager->assign($userId, $this->_name, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether removal is successful + * @see Manager::revoke + */ + public function revoke($userId) + { + return $this->manager->revoke($userId, $this->_name); + } + + /** + * Returns a value indicating whether this item has been assigned to the user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether the item has been assigned to the user. + * @see Manager::isAssigned + */ + public function isAssigned($userId) + { + return $this->manager->isAssigned($userId, $this->_name); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return Assignment the item assignment information. Null is returned if + * this item is not assigned to the user. + * @see Manager::getAssignment + */ + public function getAssignment($userId) + { + return $this->manager->getAssignment($userId, $this->_name); + } + + /** + * Saves an authorization item to persistent storage. + */ + public function save() + { + $this->manager->saveItem($this, $this->_oldName); + $this->_oldName = null; + } } diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php index aa651f3b40b..7c618a91772 100644 --- a/framework/rbac/Manager.php +++ b/framework/rbac/Manager.php @@ -43,276 +43,276 @@ */ abstract class Manager extends Component { - /** - * @var boolean Enable error reporting for bizRules. - */ - public $showErrors = false; + /** + * @var boolean Enable error reporting for bizRules. + */ + public $showErrors = false; - /** - * @var array list of role names that are assigned to all users implicitly. - * These roles do not need to be explicitly assigned to any user. - * When calling [[checkAccess()]], these roles will be checked first. - * For performance reason, you should minimize the number of such roles. - * A typical usage of such roles is to define an 'authenticated' role and associate - * it with a biz rule which checks if the current user is authenticated. - * And then declare 'authenticated' in this property so that it can be applied to - * every authenticated user. - */ - public $defaultRoles = []; + /** + * @var array list of role names that are assigned to all users implicitly. + * These roles do not need to be explicitly assigned to any user. + * When calling [[checkAccess()]], these roles will be checked first. + * For performance reason, you should minimize the number of such roles. + * A typical usage of such roles is to define an 'authenticated' role and associate + * it with a biz rule which checks if the current user is authenticated. + * And then declare 'authenticated' in this property so that it can be applied to + * every authenticated user. + */ + public $defaultRoles = []; - /** - * Creates a role. - * This is a shortcut method to [[Manager::createItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return Item the authorization item - */ - public function createRole($name, $description = '', $bizRule = null, $data = null) - { - return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); - } + /** + * Creates a role. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createRole($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); + } - /** - * Creates a task. - * This is a shortcut method to [[Manager::createItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return Item the authorization item - */ - public function createTask($name, $description = '', $bizRule = null, $data = null) - { - return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); - } + /** + * Creates a task. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createTask($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); + } - /** - * Creates an operation. - * This is a shortcut method to [[Manager::createItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return Item the authorization item - */ - public function createOperation($name, $description = '', $bizRule = null, $data = null) - { - return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); - } + /** + * Creates an operation. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createOperation($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); + } - /** - * Returns roles. - * This is a shortcut method to [[Manager::getItems()]]. - * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user - * will be returned. Otherwise, all roles will be returned. - * @return Item[] roles (name => AuthItem) - */ - public function getRoles($userId = null) - { - return $this->getItems($userId, Item::TYPE_ROLE); - } + /** + * Returns roles. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user + * will be returned. Otherwise, all roles will be returned. + * @return Item[] roles (name => AuthItem) + */ + public function getRoles($userId = null) + { + return $this->getItems($userId, Item::TYPE_ROLE); + } - /** - * Returns tasks. - * This is a shortcut method to [[Manager::getItems()]]. - * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user - * will be returned. Otherwise, all tasks will be returned. - * @return Item[] tasks (name => AuthItem) - */ - public function getTasks($userId = null) - { - return $this->getItems($userId, Item::TYPE_TASK); - } + /** + * Returns tasks. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user + * will be returned. Otherwise, all tasks will be returned. + * @return Item[] tasks (name => AuthItem) + */ + public function getTasks($userId = null) + { + return $this->getItems($userId, Item::TYPE_TASK); + } - /** - * Returns operations. - * This is a shortcut method to [[Manager::getItems()]]. - * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user - * will be returned. Otherwise, all operations will be returned. - * @return Item[] operations (name => AuthItem) - */ - public function getOperations($userId = null) - { - return $this->getItems($userId, Item::TYPE_OPERATION); - } + /** + * Returns operations. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user + * will be returned. Otherwise, all operations will be returned. + * @return Item[] operations (name => AuthItem) + */ + public function getOperations($userId = null) + { + return $this->getItems($userId, Item::TYPE_OPERATION); + } - /** - * Executes the specified business rule. - * @param string $bizRule the business rule to be executed. - * @param array $params parameters passed to [[Manager::checkAccess()]]. - * @param mixed $data additional data associated with the authorization item or assignment. - * @return boolean whether the business rule returns true. - * If the business rule is empty, it will still return true. - */ - public function executeBizRule($bizRule, $params, $data) - { - return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); - } + /** + * Executes the specified business rule. + * @param string $bizRule the business rule to be executed. + * @param array $params parameters passed to [[Manager::checkAccess()]]. + * @param mixed $data additional data associated with the authorization item or assignment. + * @return boolean whether the business rule returns true. + * If the business rule is empty, it will still return true. + */ + public function executeBizRule($bizRule, $params, $data) + { + return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); + } - /** - * Checks the item types to make sure a child can be added to a parent. - * @param integer $parentType parent item type - * @param integer $childType child item type - * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. - */ - protected function checkItemChildType($parentType, $childType) - { - static $types = ['operation', 'task', 'role']; - if ($parentType < $childType) { - throw new InvalidParamException("Cannot add an item of type '{$types[$childType]}' to an item of type '{$types[$parentType]}'."); - } - } + /** + * Checks the item types to make sure a child can be added to a parent. + * @param integer $parentType parent item type + * @param integer $childType child item type + * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. + */ + protected function checkItemChildType($parentType, $childType) + { + static $types = ['operation', 'task', 'role']; + if ($parentType < $childType) { + throw new InvalidParamException("Cannot add an item of type '{$types[$childType]}' to an item of type '{$types[$parentType]}'."); + } + } - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This should be either an integer or a string representing - * the unique identifier of a user. See [[\yii\web\User::id]]. - * @param string $itemName the name of the operation that we are checking access to - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. - * @return boolean whether the operations can be performed by the user. - */ - abstract public function checkAccess($userId, $itemName, $params = []); + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[\yii\web\User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + abstract public function checkAccess($userId, $itemName, $params = []); - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @throws \yii\base\Exception if an item with the same name already exists - * @return Item the authorization item - */ - abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - abstract public function removeItem($name); - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - abstract public function getItems($userId = null, $type = null); - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - abstract public function getItem($name); - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - */ - abstract public function saveItem($item, $oldName = null); + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return Item the authorization item + */ + abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + abstract public function removeItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + abstract public function getItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + abstract public function getItem($name); + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + abstract public function saveItem($item, $oldName = null); - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - */ - abstract public function addItemChild($itemName, $childName); - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - abstract public function removeItemChild($itemName, $childName); - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - abstract public function hasItemChild($itemName, $childName); - /** - * Returns the children of the specified item. - * @param mixed $itemName the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - abstract public function getItemChildren($itemName); + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + abstract public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + abstract public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + abstract public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + abstract public function getItemChildren($itemName); - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user - */ - abstract public function assign($userId, $itemName, $bizRule = null, $data = null); - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - abstract public function revoke($userId, $itemName); - /** - * Revokes all authorization assignments from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - */ - abstract public function revokeAll($userId); - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - abstract public function isAssigned($userId, $itemName); - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - abstract public function getAssignment($userId, $itemName); - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return Item[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - abstract public function getAssignments($userId); - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - abstract public function saveAssignment($assignment); - /** - * Removes all authorization data. - */ - abstract public function clearAll(); - /** - * Removes all authorization assignments. - */ - abstract public function clearAssignments(); - /** - * Saves authorization data into persistent storage. - * If any change is made to the authorization data, please make - * sure you call this method to save the changed data into persistent storage. - */ - abstract public function save(); + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + abstract public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + abstract public function revoke($userId, $itemName); + /** + * Revokes all authorization assignments from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether removal is successful + */ + abstract public function revokeAll($userId); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + abstract public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + abstract public function getAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return Item[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + abstract public function getAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + abstract public function saveAssignment($assignment); + /** + * Removes all authorization data. + */ + abstract public function clearAll(); + /** + * Removes all authorization assignments. + */ + abstract public function clearAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + abstract public function save(); } diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index bceb15b9f24..45f01f02d36 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -31,524 +31,534 @@ */ class PhpManager extends Manager { - /** - * @var string the path of the PHP script that contains the authorization data. - * This can be either a file path or a path alias to the file. - * Make sure this file is writable by the Web server process if the authorization needs to be changed online. - * @see loadFromFile() - * @see saveToFile() - */ - public $authFile = '@app/data/rbac.php'; - - private $_items = []; // itemName => item - private $_children = []; // itemName, childName => child - private $_assignments = []; // userId, itemName => assignment - - /** - * Initializes the application component. - * This method overrides parent implementation by loading the authorization data - * from PHP script. - */ - public function init() - { - parent::init(); - $this->authFile = Yii::getAlias($this->authFile); - $this->load(); - } - - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This can be either an integer or a string representing - * @param string $itemName the name of the operation that need access check - * the unique identifier of a user. See [[\yii\web\User::id]]. - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to - * this array, which holds the value of `$userId`. - * @return boolean whether the operations can be performed by the user. - */ - public function checkAccess($userId, $itemName, $params = []) - { - if (!isset($this->_items[$itemName])) { - return false; - } - /** @var Item $item */ - $item = $this->_items[$itemName]; - Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); - if (!isset($params['userId'])) { - $params['userId'] = $userId; - } - if ($this->executeBizRule($item->bizRule, $params, $item->data)) { - if (in_array($itemName, $this->defaultRoles)) { - return true; - } - if (isset($this->_assignments[$userId][$itemName])) { - /** @var Assignment $assignment */ - $assignment = $this->_assignments[$userId][$itemName]; - if ($this->executeBizRule($assignment->bizRule, $params, $assignment->data)) { - return true; - } - } - foreach ($this->_children as $parentName => $children) { - if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { - return true; - } - } - } - return false; - } - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the item is added successfully - * @throws Exception if either parent or child doesn't exist. - * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. - */ - public function addItemChild($itemName, $childName) - { - if (!isset($this->_items[$childName], $this->_items[$itemName])) { - throw new Exception("Either '$itemName' or '$childName' does not exist."); - } - /** @var Item $child */ - $child = $this->_items[$childName]; - /** @var Item $item */ - $item = $this->_items[$itemName]; - $this->checkItemChildType($item->type, $child->type); - if ($this->detectLoop($itemName, $childName)) { - throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); - } - if (isset($this->_children[$itemName][$childName])) { - throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); - } - $this->_children[$itemName][$childName] = $this->_items[$childName]; - return true; - } - - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - public function removeItemChild($itemName, $childName) - { - if (isset($this->_children[$itemName][$childName])) { - unset($this->_children[$itemName][$childName]); - return true; - } else { - return false; - } - } - - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - public function hasItemChild($itemName, $childName) - { - return isset($this->_children[$itemName][$childName]); - } - - /** - * Returns the children of the specified item. - * @param mixed $names the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - public function getItemChildren($names) - { - if (is_string($names)) { - return isset($this->_children[$names]) ? $this->_children[$names] : []; - } - - $children = []; - foreach ($names as $name) { - if (isset($this->_children[$name])) { - $children = array_merge($children, $this->_children[$name]); - } - } - return $children; - } - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user - */ - public function assign($userId, $itemName, $bizRule = null, $data = null) - { - if (!isset($this->_items[$itemName])) { - throw new InvalidParamException("Unknown authorization item '$itemName'."); - } elseif (isset($this->_assignments[$userId][$itemName])) { - throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); - } else { - return $this->_assignments[$userId][$itemName] = new Assignment([ - 'manager' => $this, - 'userId' => $userId, - 'itemName' => $itemName, - 'bizRule' => $bizRule, - 'data' => $data, - ]); - } - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - public function revoke($userId, $itemName) - { - if (isset($this->_assignments[$userId][$itemName])) { - unset($this->_assignments[$userId][$itemName]); - return true; - } else { - return false; - } - } - - /** - * Revokes all authorization assignments from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - */ - public function revokeAll($userId) - { - if (isset($this->_assignments[$userId]) && is_array($this->_assignments[$userId])) { - foreach ($this->_assignments[$userId] as $itemName => $value) - unset($this->_assignments[$userId][$itemName]); - return true; - } else { - return false; - } - } - - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - public function isAssigned($userId, $itemName) - { - return isset($this->_assignments[$userId][$itemName]); - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - public function getAssignment($userId, $itemName) - { - return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; - } - - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return Assignment[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - public function getAssignments($userId) - { - return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : []; - } - - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - public function getItems($userId = null, $type = null) - { - if ($userId === null && $type === null) { - return $this->_items; - } - $items = []; - if ($userId === null) { - foreach ($this->_items as $name => $item) { - /** @var Item $item */ - if ($item->type == $type) { - $items[$name] = $item; - } - } - } elseif (isset($this->_assignments[$userId])) { - foreach ($this->_assignments[$userId] as $assignment) { - /** @var Assignment $assignment */ - $name = $assignment->itemName; - if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->type == $type)) { - $items[$name] = $this->_items[$name]; - } - } - } - return $items; - } - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @return Item the authorization item - * @throws Exception if an item with the same name already exists - */ - public function createItem($name, $type, $description = '', $bizRule = null, $data = null) - { - if (isset($this->_items[$name])) { - throw new Exception('Unable to add an item whose name is the same as an existing item.'); - } - return $this->_items[$name] = new Item([ - 'manager' => $this, - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'bizRule' => $bizRule, - 'data' => $data, - ]); - } - - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - public function removeItem($name) - { - if (isset($this->_items[$name])) { - foreach ($this->_children as &$children) { - unset($children[$name]); - } - foreach ($this->_assignments as &$assignments) { - unset($assignments[$name]); - } - unset($this->_items[$name]); - return true; - } else { - return false; - } - } - - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - public function getItem($name) - { - return isset($this->_items[$name]) ? $this->_items[$name] : null; - } - - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - * @throws InvalidParamException if an item with the same name already taken - */ - public function saveItem($item, $oldName = null) - { - if ($oldName !== null && ($newName = $item->getName()) !== $oldName) { // name changed - if (isset($this->_items[$newName])) { - throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); - } - if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { - unset($this->_items[$oldName]); - $this->_items[$newName] = $item; - if (isset($this->_children[$oldName])) { - $this->_children[$newName] = $this->_children[$oldName]; - unset($this->_children[$oldName]); - } - foreach ($this->_children as &$children) { - if (isset($children[$oldName])) { - $children[$newName] = $children[$oldName]; - unset($children[$oldName]); - } - } - foreach ($this->_assignments as &$assignments) { - if (isset($assignments[$oldName])) { - $assignments[$newName] = $assignments[$oldName]; - unset($assignments[$oldName]); - } - } - } - } - } - - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - public function saveAssignment($assignment) - { - } - - /** - * Saves authorization data into persistent storage. - * If any change is made to the authorization data, please make - * sure you call this method to save the changed data into persistent storage. - */ - public function save() - { - $items = []; - foreach ($this->_items as $name => $item) { - /** @var Item $item */ - $items[$name] = [ - 'type' => $item->type, - 'description' => $item->description, - 'bizRule' => $item->bizRule, - 'data' => $item->data, - ]; - if (isset($this->_children[$name])) { - foreach ($this->_children[$name] as $child) { - /** @var Item $child */ - $items[$name]['children'][] = $child->getName(); - } - } - } - - foreach ($this->_assignments as $userId => $assignments) { - foreach ($assignments as $name => $assignment) { - /** @var Assignment $assignment */ - if (isset($items[$name])) { - $items[$name]['assignments'][$userId] = [ - 'bizRule' => $assignment->bizRule, - 'data' => $assignment->data, - ]; - } - } - } - - $this->saveToFile($items, $this->authFile); - } - - /** - * Loads authorization data. - */ - public function load() - { - $this->clearAll(); - - $items = $this->loadFromFile($this->authFile); - - foreach ($items as $name => $item) { - $this->_items[$name] = new Item([ - 'manager' => $this, - 'name' => $name, - 'type' => $item['type'], - 'description' => $item['description'], - 'bizRule' => $item['bizRule'], - 'data' => $item['data'], - ]); - } - - foreach ($items as $name => $item) { - if (isset($item['children'])) { - foreach ($item['children'] as $childName) { - if (isset($this->_items[$childName])) { - $this->_children[$name][$childName] = $this->_items[$childName]; - } - } - } - if (isset($item['assignments'])) { - foreach ($item['assignments'] as $userId => $assignment) { - $this->_assignments[$userId][$name] = new Assignment([ - 'manager' => $this, - 'userId' => $userId, - 'itemName' => $name, - 'bizRule' => $assignment['bizRule'], - 'data' => $assignment['data'], - ]); - } - } - } - } - - /** - * Removes all authorization data. - */ - public function clearAll() - { - $this->clearAssignments(); - $this->_children = []; - $this->_items = []; - } - - /** - * Removes all authorization assignments. - */ - public function clearAssignments() - { - $this->_assignments = []; - } - - /** - * Checks whether there is a loop in the authorization item hierarchy. - * @param string $itemName parent item name - * @param string $childName the name of the child item that is to be added to the hierarchy - * @return boolean whether a loop exists - */ - protected function detectLoop($itemName, $childName) - { - if ($childName === $itemName) { - return true; - } - if (!isset($this->_children[$childName], $this->_items[$itemName])) { - return false; - } - foreach ($this->_children[$childName] as $child) { - /** @var Item $child */ - if ($this->detectLoop($itemName, $child->getName())) { - return true; - } - } - return false; - } - - /** - * Loads the authorization data from a PHP script file. - * @param string $file the file path. - * @return array the authorization data - * @see saveToFile() - */ - protected function loadFromFile($file) - { - if (is_file($file)) { - return require($file); - } else { - return []; - } - } - - /** - * Saves the authorization data to a PHP script file. - * @param array $data the authorization data - * @param string $file the file path. - * @see loadFromFile() - */ - protected function saveToFile($data, $file) - { - file_put_contents($file, " item + private $_children = []; // itemName, childName => child + private $_assignments = []; // userId, itemName => assignment + + /** + * Initializes the application component. + * This method overrides parent implementation by loading the authorization data + * from PHP script. + */ + public function init() + { + parent::init(); + $this->authFile = Yii::getAlias($this->authFile); + $this->load(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This can be either an integer or a string representing + * @param string $itemName the name of the operation that need access check + * the unique identifier of a user. See [[\yii\web\User::id]]. + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = []) + { + if (!isset($this->_items[$itemName])) { + return false; + } + /** @var Item $item */ + $item = $this->_items[$itemName]; + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->bizRule, $params, $item->data)) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($this->_assignments[$userId][$itemName])) { + /** @var Assignment $assignment */ + $assignment = $this->_assignments[$userId][$itemName]; + if ($this->executeBizRule($assignment->bizRule, $params, $assignment->data)) { + return true; + } + } + foreach ($this->_children as $parentName => $children) { + if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { + return true; + } + } + } + + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if (!isset($this->_items[$childName], $this->_items[$itemName])) { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + /** @var Item $child */ + $child = $this->_items[$childName]; + /** @var Item $item */ + $item = $this->_items[$itemName]; + $this->checkItemChildType($item->type, $child->type); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + if (isset($this->_children[$itemName][$childName])) { + throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); + } + $this->_children[$itemName][$childName] = $this->_items[$childName]; + + return true; + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + if (isset($this->_children[$itemName][$childName])) { + unset($this->_children[$itemName][$childName]); + + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + return isset($this->_children[$itemName][$childName]); + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + if (is_string($names)) { + return isset($this->_children[$names]) ? $this->_children[$names] : []; + } + + $children = []; + foreach ($names as $name) { + if (isset($this->_children[$name])) { + $children = array_merge($children, $this->_children[$name]); + } + } + + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if (!isset($this->_items[$itemName])) { + throw new InvalidParamException("Unknown authorization item '$itemName'."); + } elseif (isset($this->_assignments[$userId][$itemName])) { + throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); + } else { + return $this->_assignments[$userId][$itemName] = new Assignment([ + 'manager' => $this, + 'userId' => $userId, + 'itemName' => $itemName, + 'bizRule' => $bizRule, + 'data' => $data, + ]); + } + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + if (isset($this->_assignments[$userId][$itemName])) { + unset($this->_assignments[$userId][$itemName]); + + return true; + } else { + return false; + } + } + + /** + * Revokes all authorization assignments from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether removal is successful + */ + public function revokeAll($userId) + { + if (isset($this->_assignments[$userId]) && is_array($this->_assignments[$userId])) { + foreach ($this->_assignments[$userId] as $itemName => $value) + unset($this->_assignments[$userId][$itemName]); + + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : []; + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + if ($userId === null && $type === null) { + return $this->_items; + } + $items = []; + if ($userId === null) { + foreach ($this->_items as $name => $item) { + /** @var Item $item */ + if ($item->type == $type) { + $items[$name] = $item; + } + } + } elseif (isset($this->_assignments[$userId])) { + foreach ($this->_assignments[$userId] as $assignment) { + /** @var Assignment $assignment */ + $name = $assignment->itemName; + if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->type == $type)) { + $items[$name] = $this->_items[$name]; + } + } + } + + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + if (isset($this->_items[$name])) { + throw new Exception('Unable to add an item whose name is the same as an existing item.'); + } + + return $this->_items[$name] = new Item([ + 'manager' => $this, + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizRule' => $bizRule, + 'data' => $data, + ]); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if (isset($this->_items[$name])) { + foreach ($this->_children as &$children) { + unset($children[$name]); + } + foreach ($this->_assignments as &$assignments) { + unset($assignments[$name]); + } + unset($this->_items[$name]); + + return true; + } else { + return false; + } + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + return isset($this->_items[$name]) ? $this->_items[$name] : null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + * @throws InvalidParamException if an item with the same name already taken + */ + public function saveItem($item, $oldName = null) + { + if ($oldName !== null && ($newName = $item->getName()) !== $oldName) { // name changed + if (isset($this->_items[$newName])) { + throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); + } + if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { + unset($this->_items[$oldName]); + $this->_items[$newName] = $item; + if (isset($this->_children[$oldName])) { + $this->_children[$newName] = $this->_children[$oldName]; + unset($this->_children[$oldName]); + } + foreach ($this->_children as &$children) { + if (isset($children[$oldName])) { + $children[$newName] = $children[$oldName]; + unset($children[$oldName]); + } + } + foreach ($this->_assignments as &$assignments) { + if (isset($assignments[$oldName])) { + $assignments[$newName] = $assignments[$oldName]; + unset($assignments[$oldName]); + } + } + } + } + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + } + + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save() + { + $items = []; + foreach ($this->_items as $name => $item) { + /** @var Item $item */ + $items[$name] = [ + 'type' => $item->type, + 'description' => $item->description, + 'bizRule' => $item->bizRule, + 'data' => $item->data, + ]; + if (isset($this->_children[$name])) { + foreach ($this->_children[$name] as $child) { + /** @var Item $child */ + $items[$name]['children'][] = $child->getName(); + } + } + } + + foreach ($this->_assignments as $userId => $assignments) { + foreach ($assignments as $name => $assignment) { + /** @var Assignment $assignment */ + if (isset($items[$name])) { + $items[$name]['assignments'][$userId] = [ + 'bizRule' => $assignment->bizRule, + 'data' => $assignment->data, + ]; + } + } + } + + $this->saveToFile($items, $this->authFile); + } + + /** + * Loads authorization data. + */ + public function load() + { + $this->clearAll(); + + $items = $this->loadFromFile($this->authFile); + + foreach ($items as $name => $item) { + $this->_items[$name] = new Item([ + 'manager' => $this, + 'name' => $name, + 'type' => $item['type'], + 'description' => $item['description'], + 'bizRule' => $item['bizRule'], + 'data' => $item['data'], + ]); + } + + foreach ($items as $name => $item) { + if (isset($item['children'])) { + foreach ($item['children'] as $childName) { + if (isset($this->_items[$childName])) { + $this->_children[$name][$childName] = $this->_items[$childName]; + } + } + } + if (isset($item['assignments'])) { + foreach ($item['assignments'] as $userId => $assignment) { + $this->_assignments[$userId][$name] = new Assignment([ + 'manager' => $this, + 'userId' => $userId, + 'itemName' => $name, + 'bizRule' => $assignment['bizRule'], + 'data' => $assignment['data'], + ]); + } + } + } + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->_children = []; + $this->_items = []; + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->_assignments = []; + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + if (!isset($this->_children[$childName], $this->_items[$itemName])) { + return false; + } + foreach ($this->_children[$childName] as $child) { + /** @var Item $child */ + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + + return false; + } + + /** + * Loads the authorization data from a PHP script file. + * @param string $file the file path. + * @return array the authorization data + * @see saveToFile() + */ + protected function loadFromFile($file) + { + if (is_file($file)) { + return require($file); + } else { + return []; + } + } + + /** + * Saves the authorization data to a PHP script file. + * @param array $data the authorization data + * @param string $file the file path. + * @see loadFromFile() + */ + protected function saveToFile($data, $file) + { + file_put_contents($file, "usageError('Requirements must be an array, "' . gettype($requirements) . '" has been given!'); - } - if (!isset($this->result) || !is_array($this->result)) { - $this->result = array( - 'summary' => array( - 'total' => 0, - 'errors' => 0, - 'warnings' => 0, - ), - 'requirements' => array(), - ); - } - foreach ($requirements as $key => $rawRequirement) { - $requirement = $this->normalizeRequirement($rawRequirement, $key); - $this->result['summary']['total']++; - if (!$requirement['condition']) { - if ($requirement['mandatory']) { - $requirement['error'] = true; - $requirement['warning'] = true; - $this->result['summary']['errors']++; - } else { - $requirement['error'] = false; - $requirement['warning'] = true; - $this->result['summary']['warnings']++; - } - } else { - $requirement['error'] = false; - $requirement['warning'] = false; - } - $this->result['requirements'][] = $requirement; - } - return $this; - } + /** + * Check the given requirements, collecting results into internal field. + * This method can be invoked several times checking different requirement sets. + * Use [[getResult()]] or [[render()]] to get the results. + * @param array|string $requirements requirements to be checked. + * If an array, it is treated as the set of requirements; + * If a string, it is treated as the path of the file, which contains the requirements; + * @return static self instance. + */ + public function check($requirements) + { + if (is_string($requirements)) { + $requirements = require($requirements); + } + if (!is_array($requirements)) { + $this->usageError('Requirements must be an array, "' . gettype($requirements) . '" has been given!'); + } + if (!isset($this->result) || !is_array($this->result)) { + $this->result = array( + 'summary' => array( + 'total' => 0, + 'errors' => 0, + 'warnings' => 0, + ), + 'requirements' => array(), + ); + } + foreach ($requirements as $key => $rawRequirement) { + $requirement = $this->normalizeRequirement($rawRequirement, $key); + $this->result['summary']['total']++; + if (!$requirement['condition']) { + if ($requirement['mandatory']) { + $requirement['error'] = true; + $requirement['warning'] = true; + $this->result['summary']['errors']++; + } else { + $requirement['error'] = false; + $requirement['warning'] = true; + $this->result['summary']['warnings']++; + } + } else { + $requirement['error'] = false; + $requirement['warning'] = false; + } + $this->result['requirements'][] = $requirement; + } - /** - * Performs the check for the Yii core requirements. - * @return YiiRequirementChecker self instance. - */ - function checkYii() - { - return $this->check(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'requirements.php'); - } + return $this; + } - /** - * Return the check results. - * @return array|null check results in format: - * - * ```php - * array( - * 'summary' => array( - * 'total' => total number of checks, - * 'errors' => number of errors, - * 'warnings' => number of warnings, - * ), - * 'requirements' => array( - * array( - * ... - * 'error' => is there an error, - * 'warning' => is there a warning, - * ), - * ... - * ), - * ) - * ``` - */ - function getResult() - { - if (isset($this->result)) { - return $this->result; - } else { - return null; - } - } + /** + * Performs the check for the Yii core requirements. + * @return YiiRequirementChecker self instance. + */ + public function checkYii() + { + return $this->check(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'requirements.php'); + } - /** - * Renders the requirements check result. - * The output will vary depending is a script running from web or from console. - */ - function render() - { - if (!isset($this->result)) { - $this->usageError('Nothing to render!'); - } - $baseViewFilePath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'views'; - if (!empty($_SERVER['argv'])) { - $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'console' . DIRECTORY_SEPARATOR . 'index.php'; - } else { - $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'index.php'; - } - $this->renderViewFile($viewFileName, $this->result); - } + /** + * Return the check results. + * @return array|null check results in format: + * + * ```php + * array( + * 'summary' => array( + * 'total' => total number of checks, + * 'errors' => number of errors, + * 'warnings' => number of warnings, + * ), + * 'requirements' => array( + * array( + * ... + * 'error' => is there an error, + * 'warning' => is there a warning, + * ), + * ... + * ), + * ) + * ``` + */ + public function getResult() + { + if (isset($this->result)) { + return $this->result; + } else { + return null; + } + } - /** - * Checks if the given PHP extension is available and its version matches the given one. - * @param string $extensionName PHP extension name. - * @param string $version required PHP extension version. - * @param string $compare comparison operator, by default '>=' - * @return boolean if PHP extension version matches. - */ - function checkPhpExtensionVersion($extensionName, $version, $compare = '>=') - { - if (!extension_loaded($extensionName)) { - return false; - } - $extensionVersion = phpversion($extensionName); - if (empty($extensionVersion)) { - return false; - } - if (strncasecmp($extensionVersion, 'PECL-', 5) == 0) { - $extensionVersion = substr($extensionVersion, 5); - } - return version_compare($extensionVersion, $version, $compare); - } + /** + * Renders the requirements check result. + * The output will vary depending is a script running from web or from console. + */ + public function render() + { + if (!isset($this->result)) { + $this->usageError('Nothing to render!'); + } + $baseViewFilePath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'views'; + if (!empty($_SERVER['argv'])) { + $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'console' . DIRECTORY_SEPARATOR . 'index.php'; + } else { + $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'index.php'; + } + $this->renderViewFile($viewFileName, $this->result); + } - /** - * Checks if PHP configuration option (from php.ini) is on. - * @param string $name configuration option name. - * @return boolean option is on. - */ - function checkPhpIniOn($name) - { - $value = ini_get($name); - if (empty($value)) { - return false; - } - return ((integer)$value == 1 || strtolower($value) == 'on'); - } + /** + * Checks if the given PHP extension is available and its version matches the given one. + * @param string $extensionName PHP extension name. + * @param string $version required PHP extension version. + * @param string $compare comparison operator, by default '>=' + * @return boolean if PHP extension version matches. + */ + public function checkPhpExtensionVersion($extensionName, $version, $compare = '>=') + { + if (!extension_loaded($extensionName)) { + return false; + } + $extensionVersion = phpversion($extensionName); + if (empty($extensionVersion)) { + return false; + } + if (strncasecmp($extensionVersion, 'PECL-', 5) == 0) { + $extensionVersion = substr($extensionVersion, 5); + } - /** - * Checks if PHP configuration option (from php.ini) is off. - * @param string $name configuration option name. - * @return boolean option is off. - */ - function checkPhpIniOff($name) - { - $value = ini_get($name); - if (empty($value)) { - return true; - } - return (strtolower($value) == 'off'); - } + return version_compare($extensionVersion, $version, $compare); + } - /** - * Compare byte sizes of values given in the verbose representation, - * like '5M', '15K' etc. - * @param string $a first value. - * @param string $b second value. - * @param string $compare comparison operator, by default '>='. - * @return boolean comparison result. - */ - function compareByteSize($a, $b, $compare = '>=') - { - $compareExpression = '(' . $this->getByteSize($a) . $compare . $this->getByteSize($b) . ')'; - return $this->evaluateExpression($compareExpression); - } + /** + * Checks if PHP configuration option (from php.ini) is on. + * @param string $name configuration option name. + * @return boolean option is on. + */ + public function checkPhpIniOn($name) + { + $value = ini_get($name); + if (empty($value)) { + return false; + } - /** - * Gets the size in bytes from verbose size representation. - * For example: '5K' => 5*1024 - * @param string $verboseSize verbose size representation. - * @return integer actual size in bytes. - */ - function getByteSize($verboseSize) - { - if (empty($verboseSize)) { - return 0; - } - if (is_numeric($verboseSize)) { - return (integer)$verboseSize; - } - $sizeUnit = trim($verboseSize, '0123456789'); - $size = str_replace($sizeUnit, '', $verboseSize); - $size = trim($size); - if (!is_numeric($size)) { - return 0; - } - switch (strtolower($sizeUnit)) { - case 'kb': - case 'k': { - return $size * 1024; - } - case 'mb': - case 'm': { - return $size * 1024 * 1024; - } - case 'gb': - case 'g': { - return $size * 1024 * 1024 * 1024; - } - default: { - return 0; - } - } - } + return ((integer) $value == 1 || strtolower($value) == 'on'); + } - /** - * Checks if upload max file size matches the given range. - * @param string|null $min verbose file size minimum required value, pass null to skip minimum check. - * @param string|null $max verbose file size maximum required value, pass null to skip maximum check. - * @return boolean success. - */ - function checkUploadMaxFileSize($min = null, $max = null) - { - $postMaxSize = ini_get('post_max_size'); - $uploadMaxFileSize = ini_get('upload_max_filesize'); - if ($min !== null) { - $minCheckResult = $this->compareByteSize($postMaxSize, $min, '>=') && $this->compareByteSize($uploadMaxFileSize, $min, '>='); - } else { - $minCheckResult = true; - } - if ($max !== null) { - var_dump($postMaxSize, $uploadMaxFileSize, $max); - $maxCheckResult = $this->compareByteSize($postMaxSize, $max, '<=') && $this->compareByteSize($uploadMaxFileSize, $max, '<='); - } else { - $maxCheckResult = true; - } - return ($minCheckResult && $maxCheckResult); - } + /** + * Checks if PHP configuration option (from php.ini) is off. + * @param string $name configuration option name. + * @return boolean option is off. + */ + public function checkPhpIniOff($name) + { + $value = ini_get($name); + if (empty($value)) { + return true; + } - /** - * Renders a view file. - * This method includes the view file as a PHP script - * and captures the display result if required. - * @param string $_viewFile_ view file - * @param array $_data_ data to be extracted and made available to the view file - * @param boolean $_return_ whether the rendering result should be returned as a string - * @return string the rendering result. Null if the rendering result is not required. - */ - function renderViewFile($_viewFile_, $_data_ = null, $_return_ = false) - { - // we use special variable names here to avoid conflict when extracting data - if (is_array($_data_)) { - extract($_data_, EXTR_PREFIX_SAME, 'data'); - } else { - $data = $_data_; - } - if ($_return_) { - ob_start(); - ob_implicit_flush(false); - require($_viewFile_); - return ob_get_clean(); - } else { - require($_viewFile_); - } - } + return (strtolower($value) == 'off'); + } - /** - * Normalizes requirement ensuring it has correct format. - * @param array $requirement raw requirement. - * @param integer $requirementKey requirement key in the list. - * @return array normalized requirement. - */ - function normalizeRequirement($requirement, $requirementKey = 0) - { - if (!is_array($requirement)) { - $this->usageError('Requirement must be an array!'); - } - if (!array_key_exists('condition', $requirement)) { - $this->usageError("Requirement '{$requirementKey}' has no condition!"); - } else { - $evalPrefix = 'eval:'; - if (is_string($requirement['condition']) && strpos($requirement['condition'], $evalPrefix) === 0) { - $expression = substr($requirement['condition'], strlen($evalPrefix)); - $requirement['condition'] = $this->evaluateExpression($expression); - } - } - if (!array_key_exists('name', $requirement)) { - $requirement['name'] = is_numeric($requirementKey) ? 'Requirement #' . $requirementKey : $requirementKey; - } - if (!array_key_exists('mandatory', $requirement)) { - if (array_key_exists('required', $requirement)) { - $requirement['mandatory'] = $requirement['required']; - } else { - $requirement['mandatory'] = false; - } - } - if (!array_key_exists('by', $requirement)) { - $requirement['by'] = 'Unknown'; - } - if (!array_key_exists('memo', $requirement)) { - $requirement['memo'] = ''; - } - return $requirement; - } + /** + * Compare byte sizes of values given in the verbose representation, + * like '5M', '15K' etc. + * @param string $a first value. + * @param string $b second value. + * @param string $compare comparison operator, by default '>='. + * @return boolean comparison result. + */ + public function compareByteSize($a, $b, $compare = '>=') + { + $compareExpression = '(' . $this->getByteSize($a) . $compare . $this->getByteSize($b) . ')'; - /** - * Displays a usage error. - * This method will then terminate the execution of the current application. - * @param string $message the error message - */ - function usageError($message) - { - echo "Error: $message\n\n"; - exit(1); - } + return $this->evaluateExpression($compareExpression); + } - /** - * Evaluates a PHP expression under the context of this class. - * @param string $expression a PHP expression to be evaluated. - * @return mixed the expression result. - */ - function evaluateExpression($expression) - { - return eval('return ' . $expression . ';'); - } + /** + * Gets the size in bytes from verbose size representation. + * For example: '5K' => 5*1024 + * @param string $verboseSize verbose size representation. + * @return integer actual size in bytes. + */ + public function getByteSize($verboseSize) + { + if (empty($verboseSize)) { + return 0; + } + if (is_numeric($verboseSize)) { + return (integer) $verboseSize; + } + $sizeUnit = trim($verboseSize, '0123456789'); + $size = str_replace($sizeUnit, '', $verboseSize); + $size = trim($size); + if (!is_numeric($size)) { + return 0; + } + switch (strtolower($sizeUnit)) { + case 'kb': + case 'k': { + return $size * 1024; + } + case 'mb': + case 'm': { + return $size * 1024 * 1024; + } + case 'gb': + case 'g': { + return $size * 1024 * 1024 * 1024; + } + default: { + return 0; + } + } + } - /** - * Returns the server information. - * @return string server information. - */ - function getServerInfo() - { - $info = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : ''; - return $info; - } + /** + * Checks if upload max file size matches the given range. + * @param string|null $min verbose file size minimum required value, pass null to skip minimum check. + * @param string|null $max verbose file size maximum required value, pass null to skip maximum check. + * @return boolean success. + */ + public function checkUploadMaxFileSize($min = null, $max = null) + { + $postMaxSize = ini_get('post_max_size'); + $uploadMaxFileSize = ini_get('upload_max_filesize'); + if ($min !== null) { + $minCheckResult = $this->compareByteSize($postMaxSize, $min, '>=') && $this->compareByteSize($uploadMaxFileSize, $min, '>='); + } else { + $minCheckResult = true; + } + if ($max !== null) { + var_dump($postMaxSize, $uploadMaxFileSize, $max); + $maxCheckResult = $this->compareByteSize($postMaxSize, $max, '<=') && $this->compareByteSize($uploadMaxFileSize, $max, '<='); + } else { + $maxCheckResult = true; + } - /** - * Returns the now date if possible in string representation. - * @return string now date. - */ - function getNowDate() - { - $nowDate = @strftime('%Y-%m-%d %H:%M', time()); - return $nowDate; - } + return ($minCheckResult && $maxCheckResult); + } + + /** + * Renders a view file. + * This method includes the view file as a PHP script + * and captures the display result if required. + * @param string $_viewFile_ view file + * @param array $_data_ data to be extracted and made available to the view file + * @param boolean $_return_ whether the rendering result should be returned as a string + * @return string the rendering result. Null if the rendering result is not required. + */ + public function renderViewFile($_viewFile_, $_data_ = null, $_return_ = false) + { + // we use special variable names here to avoid conflict when extracting data + if (is_array($_data_)) { + extract($_data_, EXTR_PREFIX_SAME, 'data'); + } else { + $data = $_data_; + } + if ($_return_) { + ob_start(); + ob_implicit_flush(false); + require($_viewFile_); + + return ob_get_clean(); + } else { + require($_viewFile_); + } + } + + /** + * Normalizes requirement ensuring it has correct format. + * @param array $requirement raw requirement. + * @param integer $requirementKey requirement key in the list. + * @return array normalized requirement. + */ + public function normalizeRequirement($requirement, $requirementKey = 0) + { + if (!is_array($requirement)) { + $this->usageError('Requirement must be an array!'); + } + if (!array_key_exists('condition', $requirement)) { + $this->usageError("Requirement '{$requirementKey}' has no condition!"); + } else { + $evalPrefix = 'eval:'; + if (is_string($requirement['condition']) && strpos($requirement['condition'], $evalPrefix) === 0) { + $expression = substr($requirement['condition'], strlen($evalPrefix)); + $requirement['condition'] = $this->evaluateExpression($expression); + } + } + if (!array_key_exists('name', $requirement)) { + $requirement['name'] = is_numeric($requirementKey) ? 'Requirement #' . $requirementKey : $requirementKey; + } + if (!array_key_exists('mandatory', $requirement)) { + if (array_key_exists('required', $requirement)) { + $requirement['mandatory'] = $requirement['required']; + } else { + $requirement['mandatory'] = false; + } + } + if (!array_key_exists('by', $requirement)) { + $requirement['by'] = 'Unknown'; + } + if (!array_key_exists('memo', $requirement)) { + $requirement['memo'] = ''; + } + + return $requirement; + } + + /** + * Displays a usage error. + * This method will then terminate the execution of the current application. + * @param string $message the error message + */ + public function usageError($message) + { + echo "Error: $message\n\n"; + exit(1); + } + + /** + * Evaluates a PHP expression under the context of this class. + * @param string $expression a PHP expression to be evaluated. + * @return mixed the expression result. + */ + public function evaluateExpression($expression) + { + return eval('return ' . $expression . ';'); + } + + /** + * Returns the server information. + * @return string server information. + */ + public function getServerInfo() + { + $info = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : ''; + + return $info; + } + + /** + * Returns the now date if possible in string representation. + * @return string now date. + */ + public function getNowDate() + { + $nowDate = @strftime('%Y-%m-%d %H:%M', time()); + + return $nowDate; + } } diff --git a/framework/requirements/requirements.php b/framework/requirements/requirements.php index 916d6aa1da6..e14c4069909 100644 --- a/framework/requirements/requirements.php +++ b/framework/requirements/requirements.php @@ -6,45 +6,45 @@ * @var YiiRequirementChecker $this */ return array( - array( - 'name' => 'PHP version', - 'mandatory' => true, - 'condition' => version_compare(PHP_VERSION, '5.4.0', '>='), - 'by' => 'Yii Framework', - 'memo' => 'PHP 5.4.0 or higher is required.', - ), - array( - 'name' => 'Reflection extension', - 'mandatory' => true, - 'condition' => class_exists('Reflection', false), - 'by' => 'Yii Framework', - ), - array( - 'name' => 'PCRE extension', - 'mandatory' => true, - 'condition' => extension_loaded('pcre'), - 'by' => 'Yii Framework', - ), - array( - 'name' => 'SPL extension', - 'mandatory' => true, - 'condition' => extension_loaded('SPL'), - 'by' => 'Yii Framework', - ), - array( - 'name' => 'MBString extension', - 'mandatory' => true, - 'condition' => extension_loaded('mbstring'), - 'by' => 'Multibyte string processing', - 'memo' => 'Required for multibyte encoding string processing.' - ), - array( - 'name' => 'Intl extension', - 'mandatory' => false, - 'condition' => $this->checkPhpExtensionVersion('intl', '1.0.2', '>='), - 'by' => 'Internationalization support', - 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use advanced parameters formatting - in Yii::t(), IDN-feature of - EmailValidator or UrlValidator or the yii\i18n\Formatter class.' - ), + array( + 'name' => 'PHP version', + 'mandatory' => true, + 'condition' => version_compare(PHP_VERSION, '5.4.0', '>='), + 'by' => 'Yii Framework', + 'memo' => 'PHP 5.4.0 or higher is required.', + ), + array( + 'name' => 'Reflection extension', + 'mandatory' => true, + 'condition' => class_exists('Reflection', false), + 'by' => 'Yii Framework', + ), + array( + 'name' => 'PCRE extension', + 'mandatory' => true, + 'condition' => extension_loaded('pcre'), + 'by' => 'Yii Framework', + ), + array( + 'name' => 'SPL extension', + 'mandatory' => true, + 'condition' => extension_loaded('SPL'), + 'by' => 'Yii Framework', + ), + array( + 'name' => 'MBString extension', + 'mandatory' => true, + 'condition' => extension_loaded('mbstring'), + 'by' => 'Multibyte string processing', + 'memo' => 'Required for multibyte encoding string processing.' + ), + array( + 'name' => 'Intl extension', + 'mandatory' => false, + 'condition' => $this->checkPhpExtensionVersion('intl', '1.0.2', '>='), + 'by' => 'Internationalization support', + 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use advanced parameters formatting + in Yii::t(), IDN-feature of + EmailValidator or UrlValidator or the yii\i18n\Formatter class.' + ), ); diff --git a/framework/requirements/views/console/index.php b/framework/requirements/views/console/index.php index 1d87fe956d9..1f92c6882f1 100644 --- a/framework/requirements/views/console/index.php +++ b/framework/requirements/views/console/index.php @@ -15,18 +15,18 @@ echo str_pad('', strlen($header), '-')."\n\n"; foreach ($requirements as $key => $requirement) { - if ($requirement['condition']) { - echo $requirement['name'].": OK\n"; - echo "\n"; - } else { - echo $requirement['name'].': '.($requirement['mandatory'] ? 'FAILED!!!' : 'WARNING!!!')."\n"; - echo 'Required by: '.strip_tags($requirement['by'])."\n"; - $memo = strip_tags($requirement['memo']); - if (!empty($memo)) { - echo 'Memo: '.strip_tags($requirement['memo'])."\n"; - } - echo "\n"; - } + if ($requirement['condition']) { + echo $requirement['name'].": OK\n"; + echo "\n"; + } else { + echo $requirement['name'].': '.($requirement['mandatory'] ? 'FAILED!!!' : 'WARNING!!!')."\n"; + echo 'Required by: '.strip_tags($requirement['by'])."\n"; + $memo = strip_tags($requirement['memo']); + if (!empty($memo)) { + echo 'Memo: '.strip_tags($requirement['memo'])."\n"; + } + echo "\n"; + } } $summaryString = 'Errors: '.$summary['errors'].' Warnings: '.$summary['warnings'].' Total checks: '.$summary['total']; diff --git a/framework/requirements/views/web/index.php b/framework/requirements/views/web/index.php index 7ae6aee3564..600e6d4e286 100644 --- a/framework/requirements/views/web/index.php +++ b/framework/requirements/views/web/index.php @@ -6,77 +6,77 @@ - - Yii Application Requirement Checker - renderViewFile(dirname(__FILE__) . '/css.php'); ?> + + Yii Application Requirement Checker + renderViewFile(dirname(__FILE__) . '/css.php'); ?>
    -
    -

    Yii Application Requirement Checker

    -
    -
    +
    +

    Yii Application Requirement Checker

    +
    +
    -
    -

    Description

    -

    - This script checks if your server configuration meets the requirements - for running Yii application. - It checks if the server is running the right version of PHP, - if appropriate PHP extensions have been loaded, and if php.ini file settings are correct. -

    -

    - There are two kinds of requirements being checked. Mandatory requirements are those that have to be met - to allow Yii to work as expected. There are also some optional requirements being checked which will - show you a warning when they do not meet. You can use Yii framework without them but some specific - functionality may be not available in this case. -

    +
    +

    Description

    +

    + This script checks if your server configuration meets the requirements + for running Yii application. + It checks if the server is running the right version of PHP, + if appropriate PHP extensions have been loaded, and if php.ini file settings are correct. +

    +

    + There are two kinds of requirements being checked. Mandatory requirements are those that have to be met + to allow Yii to work as expected. There are also some optional requirements being checked which will + show you a warning when they do not meet. You can use Yii framework without them but some specific + functionality may be not available in this case. +

    -

    Conclusion

    - 0): ?> -
    - Unfortunately your server configuration does not satisfy the requirements by this application.
    Please refer to the table below for detailed explanation.
    -
    - 0): ?> -
    - Your server configuration satisfies the minimum requirements by this application.
    Please pay attention to the warnings listed below and check if your application will use the corresponding features.
    -
    - -
    - Congratulations! Your server configuration satisfies all requirements. -
    - +

    Conclusion

    + 0): ?> +
    + Unfortunately your server configuration does not satisfy the requirements by this application.
    Please refer to the table below for detailed explanation.
    +
    + 0): ?> +
    + Your server configuration satisfies the minimum requirements by this application.
    Please pay attention to the warnings listed below and check if your application will use the corresponding features.
    +
    + +
    + Congratulations! Your server configuration satisfies all requirements. +
    + -

    Details

    +

    Details

    - - - - - - - - - - -
    NameResultRequired ByMemo
    - - - - - - - -
    + + + + + + + + + + +
    NameResultRequired ByMemo
    + + + + + + + +
    -
    +
    -
    +
    - +
    diff --git a/framework/rest/Action.php b/framework/rest/Action.php index 1a985397b7d..9c9d8cd2986 100644 --- a/framework/rest/Action.php +++ b/framework/rest/Action.php @@ -20,87 +20,86 @@ */ class Action extends \yii\base\Action { - /** - * @var string class name of the model which will be handled by this action. - * The model class must implement [[ActiveRecordInterface]]. - * This property must be set. - */ - public $modelClass; - /** - * @var callable a PHP callable that will be called to return the model corresponding - * to the specified primary key value. If not set, [[findModel()]] will be used instead. - * The signature of the callable should be: - * - * ```php - * function ($id, $action) { - * // $id is the primary key value. If composite primary key, the key values - * // will be separated by comma. - * // $action is the action object currently running - * } - * ``` - * - * The callable should return the model found, or throw an exception if not found. - */ - public $findModel; - /** - * @var callable a PHP callable that will be called when running an action to determine - * if the current user has the permission to execute the action. If not set, the access - * check will not be performed. The signature of the callable should be as follows, - * - * ```php - * function ($action, $model = null) { - * // $model is the requested model instance. - * // If null, it means no specific model (e.g. IndexAction) - * } - * ``` - */ - public $checkAccess; + /** + * @var string class name of the model which will be handled by this action. + * The model class must implement [[ActiveRecordInterface]]. + * This property must be set. + */ + public $modelClass; + /** + * @var callable a PHP callable that will be called to return the model corresponding + * to the specified primary key value. If not set, [[findModel()]] will be used instead. + * The signature of the callable should be: + * + * ```php + * function ($id, $action) { + * // $id is the primary key value. If composite primary key, the key values + * // will be separated by comma. + * // $action is the action object currently running + * } + * ``` + * + * The callable should return the model found, or throw an exception if not found. + */ + public $findModel; + /** + * @var callable a PHP callable that will be called when running an action to determine + * if the current user has the permission to execute the action. If not set, the access + * check will not be performed. The signature of the callable should be as follows, + * + * ```php + * function ($action, $model = null) { + * // $model is the requested model instance. + * // If null, it means no specific model (e.g. IndexAction) + * } + * ``` + */ + public $checkAccess; + /** + * @inheritdoc + */ + public function init() + { + if ($this->modelClass === null) { + throw new InvalidConfigException(get_class($this) . '::$modelClass must be set.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - if ($this->modelClass === null) { - throw new InvalidConfigException(get_class($this) . '::$modelClass must be set.'); - } - } + /** + * Returns the data model based on the primary key given. + * If the data model is not found, a 404 HTTP exception will be raised. + * @param string $id the ID of the model to be loaded. If the model has a composite primary key, + * the ID must be a string of the primary key values separated by commas. + * The order of the primary key values should follow that returned by the `primaryKey()` method + * of the model. + * @return ActiveRecordInterface the model found + * @throws NotFoundHttpException if the model cannot be found + */ + public function findModel($id) + { + if ($this->findModel !== null) { + return call_user_func($this->findModel, $id, $this); + } - /** - * Returns the data model based on the primary key given. - * If the data model is not found, a 404 HTTP exception will be raised. - * @param string $id the ID of the model to be loaded. If the model has a composite primary key, - * the ID must be a string of the primary key values separated by commas. - * The order of the primary key values should follow that returned by the `primaryKey()` method - * of the model. - * @return ActiveRecordInterface the model found - * @throws NotFoundHttpException if the model cannot be found - */ - public function findModel($id) - { - if ($this->findModel !== null) { - return call_user_func($this->findModel, $id, $this); - } + /** + * @var ActiveRecordInterface $modelClass + */ + $modelClass = $this->modelClass; + $keys = $modelClass::primaryKey(); + if (count($keys) > 1) { + $values = explode(',', $id); + if (count($keys) === count($values)) { + $model = $modelClass::find(array_combine($keys, $values)); + } + } elseif ($id !== null) { + $model = $modelClass::find($id); + } - /** - * @var ActiveRecordInterface $modelClass - */ - $modelClass = $this->modelClass; - $keys = $modelClass::primaryKey(); - if (count($keys) > 1) { - $values = explode(',', $id); - if (count($keys) === count($values)) { - $model = $modelClass::find(array_combine($keys, $values)); - } - } elseif ($id !== null) { - $model = $modelClass::find($id); - } - - if (isset($model)) { - return $model; - } else { - throw new NotFoundHttpException("Object not found: $id"); - } - } + if (isset($model)) { + return $model; + } else { + throw new NotFoundHttpException("Object not found: $id"); + } + } } diff --git a/framework/rest/ActiveController.php b/framework/rest/ActiveController.php index 75a4f556cd8..037ae4b3cde 100644 --- a/framework/rest/ActiveController.php +++ b/framework/rest/ActiveController.php @@ -36,91 +36,90 @@ */ class ActiveController extends Controller { - /** - * @var string the model class name. This property must be set. - */ - public $modelClass; - /** - * @var string the scenario used for updating a model. - * @see \yii\base\Model::scenarios() - */ - public $updateScenario = Model::SCENARIO_DEFAULT; - /** - * @var string the scenario used for creating a model. - * @see \yii\base\Model::scenarios() - */ - public $createScenario = Model::SCENARIO_DEFAULT; - /** - * @var boolean whether to use a DB transaction when creating, updating or deleting a model. - * This property is only useful for relational database. - */ - public $transactional = true; + /** + * @var string the model class name. This property must be set. + */ + public $modelClass; + /** + * @var string the scenario used for updating a model. + * @see \yii\base\Model::scenarios() + */ + public $updateScenario = Model::SCENARIO_DEFAULT; + /** + * @var string the scenario used for creating a model. + * @see \yii\base\Model::scenarios() + */ + public $createScenario = Model::SCENARIO_DEFAULT; + /** + * @var boolean whether to use a DB transaction when creating, updating or deleting a model. + * This property is only useful for relational database. + */ + public $transactional = true; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->modelClass === null) { + throw new InvalidConfigException('The "modelClass" property must be set.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->modelClass === null) { - throw new InvalidConfigException('The "modelClass" property must be set.'); - } - } + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'index' => [ + 'class' => 'yii\rest\IndexAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'view' => [ + 'class' => 'yii\rest\ViewAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + ], + 'create' => [ + 'class' => 'yii\rest\CreateAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + 'scenario' => $this->createScenario, + 'transactional' => $this->transactional, + ], + 'update' => [ + 'class' => 'yii\rest\UpdateAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + 'scenario' => $this->updateScenario, + 'transactional' => $this->transactional, + ], + 'delete' => [ + 'class' => 'yii\rest\DeleteAction', + 'modelClass' => $this->modelClass, + 'checkAccess' => [$this, 'checkAccess'], + 'transactional' => $this->transactional, + ], + 'options' => [ + 'class' => 'yii\rest\OptionsAction', + ], + ]; + } - /** - * @inheritdoc - */ - public function actions() - { - return [ - 'index' => [ - 'class' => 'yii\rest\IndexAction', - 'modelClass' => $this->modelClass, - 'checkAccess' => [$this, 'checkAccess'], - ], - 'view' => [ - 'class' => 'yii\rest\ViewAction', - 'modelClass' => $this->modelClass, - 'checkAccess' => [$this, 'checkAccess'], - ], - 'create' => [ - 'class' => 'yii\rest\CreateAction', - 'modelClass' => $this->modelClass, - 'checkAccess' => [$this, 'checkAccess'], - 'scenario' => $this->createScenario, - 'transactional' => $this->transactional, - ], - 'update' => [ - 'class' => 'yii\rest\UpdateAction', - 'modelClass' => $this->modelClass, - 'checkAccess' => [$this, 'checkAccess'], - 'scenario' => $this->updateScenario, - 'transactional' => $this->transactional, - ], - 'delete' => [ - 'class' => 'yii\rest\DeleteAction', - 'modelClass' => $this->modelClass, - 'checkAccess' => [$this, 'checkAccess'], - 'transactional' => $this->transactional, - ], - 'options' => [ - 'class' => 'yii\rest\OptionsAction', - ], - ]; - } - - /** - * @inheritdoc - */ - protected function verbs() - { - return [ - 'index' => ['GET', 'HEAD'], - 'view' => ['GET', 'HEAD'], - 'create' => ['POST'], - 'update' => ['PUT', 'PATCH'], - 'delete' => ['DELETE'], - ]; - } + /** + * @inheritdoc + */ + protected function verbs() + { + return [ + 'index' => ['GET', 'HEAD'], + 'view' => ['GET', 'HEAD'], + 'create' => ['POST'], + 'update' => ['PUT', 'PATCH'], + 'delete' => ['DELETE'], + ]; + } } diff --git a/framework/rest/AuthInterface.php b/framework/rest/AuthInterface.php index 30ccc9f1e4e..da7a22d6779 100644 --- a/framework/rest/AuthInterface.php +++ b/framework/rest/AuthInterface.php @@ -21,21 +21,21 @@ */ interface AuthInterface { - /** - * Authenticates the current user. - * - * @param User $user - * @param Request $request - * @param Response $response - * @return IdentityInterface the authenticated user identity. If authentication information is not provided, null will be returned. - * @throws UnauthorizedHttpException if authentication information is provided but is invalid. - */ - public function authenticate($user, $request, $response); - /** - * Handles authentication failure. - * The implementation should normally throw UnauthorizedHttpException to indicate authentication failure. - * @param Response $response - * @throws UnauthorizedHttpException - */ - public function handleFailure($response); + /** + * Authenticates the current user. + * + * @param User $user + * @param Request $request + * @param Response $response + * @return IdentityInterface the authenticated user identity. If authentication information is not provided, null will be returned. + * @throws UnauthorizedHttpException if authentication information is provided but is invalid. + */ + public function authenticate($user, $request, $response); + /** + * Handles authentication failure. + * The implementation should normally throw UnauthorizedHttpException to indicate authentication failure. + * @param Response $response + * @throws UnauthorizedHttpException + */ + public function handleFailure($response); } diff --git a/framework/rest/Controller.php b/framework/rest/Controller.php index 7900b15f908..3843a0db5da 100644 --- a/framework/rest/Controller.php +++ b/framework/rest/Controller.php @@ -31,217 +31,220 @@ */ class Controller extends \yii\web\Controller { - /** - * @var string the name of the header parameter representing the API version number. - */ - public $versionHeaderParam = 'version'; - /** - * @var string|array the configuration for creating the serializer that formats the response data. - */ - public $serializer = 'yii\rest\Serializer'; - /** - * @inheritdoc - */ - public $enableCsrfValidation = false; - /** - * @var array the supported authentication methods. This property should take a list of supported - * authentication methods, each represented by an authentication class or configuration. - * If this is not set or empty, it means authentication is disabled. - */ - public $authMethods; - /** - * @var string|array the rate limiter class or configuration. If this is not set or empty, - * the rate limiting will be disabled. Note that if the user is not authenticated, the rate limiting - * will also NOT be performed. - * @see checkRateLimit() - * @see authMethods - */ - public $rateLimiter = 'yii\rest\RateLimiter'; - /** - * @var string the chosen API version number, or null if [[supportedVersions]] is empty. - * @see supportedVersions - */ - public $version; - /** - * @var array list of supported API version numbers. If the current request does not specify a version - * number, the first element will be used as the [[version|chosen version number]]. For this reason, you should - * put the latest version number at the first. If this property is empty, [[version]] will not be set. - */ - public $supportedVersions = []; - /** - * @var array list of supported response formats. The array keys are the requested content MIME types, - * and the array values are the corresponding response formats. The first element will be used - * as the response format if the current request does not specify a content type. - */ - public $supportedFormats = [ - 'application/json' => Response::FORMAT_JSON, - 'application/xml' => Response::FORMAT_XML, - ]; - - /** - * @inheritdoc - */ - public function behaviors() - { - return [ - 'verbFilter' => [ - 'class' => VerbFilter::className(), - 'actions' => $this->verbs(), - ], - ]; - } - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - $this->resolveFormatAndVersion(); - } - - /** - * @inheritdoc - */ - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $this->authenticate(); - $this->checkRateLimit($action); - return true; - } else { - return false; - } - } - - /** - * @inheritdoc - */ - public function afterAction($action, $result) - { - $result = parent::afterAction($action, $result); - return $this->serializeData($result); - } - - /** - * Resolves the response format and the API version number. - * @throws UnsupportedMediaTypeHttpException - */ - protected function resolveFormatAndVersion() - { - $this->version = empty($this->supportedVersions) ? null : reset($this->supportedVersions); - Yii::$app->getResponse()->format = reset($this->supportedFormats); - $types = Yii::$app->getRequest()->getAcceptableContentTypes(); - if (empty($types)) { - $types['*/*'] = []; - } - - foreach ($types as $type => $params) { - if (isset($this->supportedFormats[$type])) { - Yii::$app->getResponse()->format = $this->supportedFormats[$type]; - if (isset($params[$this->versionHeaderParam])) { - if (in_array($params[$this->versionHeaderParam], $this->supportedVersions, true)) { - $this->version = $params[$this->versionHeaderParam]; - } else { - throw new UnsupportedMediaTypeHttpException('You are requesting an invalid version number.'); - } - } - return; - } - } - - if (!isset($types['*/*'])) { - throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.'); - } - } - - /** - * Declares the allowed HTTP verbs. - * Please refer to [[VerbFilter::actions]] on how to declare the allowed verbs. - * @return array the allowed HTTP verbs. - */ - protected function verbs() - { - return []; - } - - /** - * Authenticates the user. - * This method implements the user authentication based on an access token sent through the `Authorization` HTTP header. - * @throws UnauthorizedHttpException if the user is not authenticated successfully - */ - protected function authenticate() - { - if (empty($this->authMethods)) { - return; - } - - $user = Yii::$app->getUser(); - $request = Yii::$app->getRequest(); - $response = Yii::$app->getResponse(); - foreach ($this->authMethods as $i => $auth) { - $this->authMethods[$i] = $auth = Yii::createObject($auth); - if (!$auth instanceof AuthInterface) { - throw new InvalidConfigException(get_class($auth) . ' must implement yii\rest\AuthInterface'); - } elseif ($auth->authenticate($user, $request, $response) !== null) { - return; - } - } - - /** @var AuthInterface $auth */ - $auth = reset($this->authMethods); - $auth->handleFailure($response); - } - - /** - * Ensures the rate limit is not exceeded. - * - * This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check, - * the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must - * implement [[RateLimitInterface]]. - * - * @param \yii\base\Action $action the action to be executed - * @throws TooManyRequestsHttpException if the rate limit is exceeded. - */ - protected function checkRateLimit($action) - { - if (empty($this->rateLimiter)) { - return; - } - - $identity = Yii::$app->getUser()->getIdentity(false); - if ($identity instanceof RateLimitInterface) { - /** @var RateLimiter $rateLimiter */ - $rateLimiter = Yii::createObject($this->rateLimiter); - $rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action); - } - } - - /** - * Serializes the specified data. - * The default implementation will create a serializer based on the configuration given by [[serializer]]. - * It then uses the serializer to serialize the given data. - * @param mixed $data the data to be serialized - * @return mixed the serialized data. - */ - protected function serializeData($data) - { - return Yii::createObject($this->serializer)->serialize($data); - } - - /** - * Checks the privilege of the current user. - * - * This method should be overridden to check whether the current user has the privilege - * to run the specified action against the specified data model. - * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. - * - * @param string $action the ID of the action to be executed - * @param object $model the model to be accessed. If null, it means no specific model is being accessed. - * @param array $params additional parameters - * @throws ForbiddenHttpException if the user does not have access - */ - public function checkAccess($action, $model = null, $params = []) - { - } + /** + * @var string the name of the header parameter representing the API version number. + */ + public $versionHeaderParam = 'version'; + /** + * @var string|array the configuration for creating the serializer that formats the response data. + */ + public $serializer = 'yii\rest\Serializer'; + /** + * @inheritdoc + */ + public $enableCsrfValidation = false; + /** + * @var array the supported authentication methods. This property should take a list of supported + * authentication methods, each represented by an authentication class or configuration. + * If this is not set or empty, it means authentication is disabled. + */ + public $authMethods; + /** + * @var string|array the rate limiter class or configuration. If this is not set or empty, + * the rate limiting will be disabled. Note that if the user is not authenticated, the rate limiting + * will also NOT be performed. + * @see checkRateLimit() + * @see authMethods + */ + public $rateLimiter = 'yii\rest\RateLimiter'; + /** + * @var string the chosen API version number, or null if [[supportedVersions]] is empty. + * @see supportedVersions + */ + public $version; + /** + * @var array list of supported API version numbers. If the current request does not specify a version + * number, the first element will be used as the [[version|chosen version number]]. For this reason, you should + * put the latest version number at the first. If this property is empty, [[version]] will not be set. + */ + public $supportedVersions = []; + /** + * @var array list of supported response formats. The array keys are the requested content MIME types, + * and the array values are the corresponding response formats. The first element will be used + * as the response format if the current request does not specify a content type. + */ + public $supportedFormats = [ + 'application/json' => Response::FORMAT_JSON, + 'application/xml' => Response::FORMAT_XML, + ]; + + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + 'verbFilter' => [ + 'class' => VerbFilter::className(), + 'actions' => $this->verbs(), + ], + ]; + } + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->resolveFormatAndVersion(); + } + + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $this->authenticate(); + $this->checkRateLimit($action); + + return true; + } else { + return false; + } + } + + /** + * @inheritdoc + */ + public function afterAction($action, $result) + { + $result = parent::afterAction($action, $result); + + return $this->serializeData($result); + } + + /** + * Resolves the response format and the API version number. + * @throws UnsupportedMediaTypeHttpException + */ + protected function resolveFormatAndVersion() + { + $this->version = empty($this->supportedVersions) ? null : reset($this->supportedVersions); + Yii::$app->getResponse()->format = reset($this->supportedFormats); + $types = Yii::$app->getRequest()->getAcceptableContentTypes(); + if (empty($types)) { + $types['*/*'] = []; + } + + foreach ($types as $type => $params) { + if (isset($this->supportedFormats[$type])) { + Yii::$app->getResponse()->format = $this->supportedFormats[$type]; + if (isset($params[$this->versionHeaderParam])) { + if (in_array($params[$this->versionHeaderParam], $this->supportedVersions, true)) { + $this->version = $params[$this->versionHeaderParam]; + } else { + throw new UnsupportedMediaTypeHttpException('You are requesting an invalid version number.'); + } + } + + return; + } + } + + if (!isset($types['*/*'])) { + throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.'); + } + } + + /** + * Declares the allowed HTTP verbs. + * Please refer to [[VerbFilter::actions]] on how to declare the allowed verbs. + * @return array the allowed HTTP verbs. + */ + protected function verbs() + { + return []; + } + + /** + * Authenticates the user. + * This method implements the user authentication based on an access token sent through the `Authorization` HTTP header. + * @throws UnauthorizedHttpException if the user is not authenticated successfully + */ + protected function authenticate() + { + if (empty($this->authMethods)) { + return; + } + + $user = Yii::$app->getUser(); + $request = Yii::$app->getRequest(); + $response = Yii::$app->getResponse(); + foreach ($this->authMethods as $i => $auth) { + $this->authMethods[$i] = $auth = Yii::createObject($auth); + if (!$auth instanceof AuthInterface) { + throw new InvalidConfigException(get_class($auth) . ' must implement yii\rest\AuthInterface'); + } elseif ($auth->authenticate($user, $request, $response) !== null) { + return; + } + } + + /** @var AuthInterface $auth */ + $auth = reset($this->authMethods); + $auth->handleFailure($response); + } + + /** + * Ensures the rate limit is not exceeded. + * + * This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check, + * the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must + * implement [[RateLimitInterface]]. + * + * @param \yii\base\Action $action the action to be executed + * @throws TooManyRequestsHttpException if the rate limit is exceeded. + */ + protected function checkRateLimit($action) + { + if (empty($this->rateLimiter)) { + return; + } + + $identity = Yii::$app->getUser()->getIdentity(false); + if ($identity instanceof RateLimitInterface) { + /** @var RateLimiter $rateLimiter */ + $rateLimiter = Yii::createObject($this->rateLimiter); + $rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action); + } + } + + /** + * Serializes the specified data. + * The default implementation will create a serializer based on the configuration given by [[serializer]]. + * It then uses the serializer to serialize the given data. + * @param mixed $data the data to be serialized + * @return mixed the serialized data. + */ + protected function serializeData($data) + { + return Yii::createObject($this->serializer)->serialize($data); + } + + /** + * Checks the privilege of the current user. + * + * This method should be overridden to check whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * + * @param string $action the ID of the action to be executed + * @param object $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws ForbiddenHttpException if the user does not have access + */ + public function checkAccess($action, $model = null, $params = []) + { + } } diff --git a/framework/rest/CreateAction.php b/framework/rest/CreateAction.php index 3ef8516d9ef..eb0e2de56be 100644 --- a/framework/rest/CreateAction.php +++ b/framework/rest/CreateAction.php @@ -20,62 +20,61 @@ */ class CreateAction extends Action { - /** - * @var string the scenario to be assigned to the new model before it is validated and saved. - */ - public $scenario = Model::SCENARIO_DEFAULT; - /** - * @var boolean whether to start a DB transaction when saving the model. - */ - public $transactional = true; - /** - * @var string the name of the view action. This property is need to create the URL when the mode is successfully created. - */ - public $viewAction = 'view'; + /** + * @var string the scenario to be assigned to the new model before it is validated and saved. + */ + public $scenario = Model::SCENARIO_DEFAULT; + /** + * @var boolean whether to start a DB transaction when saving the model. + */ + public $transactional = true; + /** + * @var string the name of the view action. This property is need to create the URL when the mode is successfully created. + */ + public $viewAction = 'view'; + /** + * Creates a new model. + * @return \yii\db\ActiveRecordInterface the model newly created + * @throws \Exception if there is any error when creating the model + */ + public function run() + { + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id); + } - /** - * Creates a new model. - * @return \yii\db\ActiveRecordInterface the model newly created - * @throws \Exception if there is any error when creating the model - */ - public function run() - { - if ($this->checkAccess) { - call_user_func($this->checkAccess, $this->id); - } + /** + * @var \yii\db\ActiveRecord $model + */ + $model = new $this->modelClass([ + 'scenario' => $this->scenario, + ]); - /** - * @var \yii\db\ActiveRecord $model - */ - $model = new $this->modelClass([ - 'scenario' => $this->scenario, - ]); + $model->load(Yii::$app->getRequest()->getBodyParams(), ''); - $model->load(Yii::$app->getRequest()->getBodyParams(), ''); + if ($this->transactional && $model instanceof ActiveRecord) { + if ($model->validate()) { + $transaction = $model->getDb()->beginTransaction(); + try { + $model->insert(false); + $transaction->commit(); + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } + } else { + $model->save(); + } - if ($this->transactional && $model instanceof ActiveRecord) { - if ($model->validate()) { - $transaction = $model->getDb()->beginTransaction(); - try { - $model->insert(false); - $transaction->commit(); - } catch (\Exception $e) { - $transaction->rollback(); - throw $e; - } - } - } else { - $model->save(); - } + if (!$model->hasErrors()) { + $response = Yii::$app->getResponse(); + $response->setStatusCode(201); + $id = implode(',', array_values($model->getPrimaryKey(true))); + $response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true)); + } - if (!$model->hasErrors()) { - $response = Yii::$app->getResponse(); - $response->setStatusCode(201); - $id = implode(',', array_values($model->getPrimaryKey(true))); - $response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true)); - } - - return $model; - } + return $model; + } } diff --git a/framework/rest/DeleteAction.php b/framework/rest/DeleteAction.php index a0355c8494c..b2d6d57a7c2 100644 --- a/framework/rest/DeleteAction.php +++ b/framework/rest/DeleteAction.php @@ -18,36 +18,35 @@ */ class DeleteAction extends Action { - /** - * @var boolean whether to start a DB transaction when deleting the model. - */ - public $transactional = true; - - - /** - * Deletes a model. - */ - public function run($id) - { - $model = $this->findModel($id); - - if ($this->checkAccess) { - call_user_func($this->checkAccess, $this->id, $model); - } - - if ($this->transactional && $model instanceof ActiveRecord) { - $transaction = $model->getDb()->beginTransaction(); - try { - $model->delete(); - $transaction->commit(); - } catch (\Exception $e) { - $transaction->rollback(); - throw $e; - } - } else { - $model->delete(); - } - - Yii::$app->getResponse()->setStatusCode(204); - } + /** + * @var boolean whether to start a DB transaction when deleting the model. + */ + public $transactional = true; + + /** + * Deletes a model. + */ + public function run($id) + { + $model = $this->findModel($id); + + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } + + if ($this->transactional && $model instanceof ActiveRecord) { + $transaction = $model->getDb()->beginTransaction(); + try { + $model->delete(); + $transaction->commit(); + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } else { + $model->delete(); + } + + Yii::$app->getResponse()->setStatusCode(204); + } } diff --git a/framework/rest/HttpBasicAuth.php b/framework/rest/HttpBasicAuth.php index 7a69c154855..c50ac35ce37 100644 --- a/framework/rest/HttpBasicAuth.php +++ b/framework/rest/HttpBasicAuth.php @@ -19,32 +19,33 @@ */ class HttpBasicAuth extends Component implements AuthInterface { - /** - * @var string the HTTP authentication realm - */ - public $realm = 'api'; + /** + * @var string the HTTP authentication realm + */ + public $realm = 'api'; - /** - * @inheritdoc - */ - public function authenticate($user, $request, $response) - { - if (($accessToken = $request->getAuthUser()) !== null) { - $identity = $user->loginByAccessToken($accessToken); - if ($identity !== null) { - return $identity; - } - $this->handleFailure($response); - } - return null; - } + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + if (($accessToken = $request->getAuthUser()) !== null) { + $identity = $user->loginByAccessToken($accessToken); + if ($identity !== null) { + return $identity; + } + $this->handleFailure($response); + } - /** - * @inheritdoc - */ - public function handleFailure($response) - { - $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); - throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); - } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } } diff --git a/framework/rest/HttpBearerAuth.php b/framework/rest/HttpBearerAuth.php index fa39d63084b..415851ffd45 100644 --- a/framework/rest/HttpBearerAuth.php +++ b/framework/rest/HttpBearerAuth.php @@ -19,34 +19,35 @@ */ class HttpBearerAuth extends Component implements AuthInterface { - /** - * @var string the HTTP authentication realm - */ - public $realm = 'api'; + /** + * @var string the HTTP authentication realm + */ + public $realm = 'api'; - /** - * @inheritdoc - */ - public function authenticate($user, $request, $response) - { - $authHeader = $request->getHeaders()->get('Authorization'); - if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) { - $identity = $user->loginByAccessToken($matches[1]); - if ($identity !== null) { - return $identity; - } + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + $authHeader = $request->getHeaders()->get('Authorization'); + if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) { + $identity = $user->loginByAccessToken($matches[1]); + if ($identity !== null) { + return $identity; + } - $this->handleFailure($response); - } - return null; - } + $this->handleFailure($response); + } - /** - * @inheritdoc - */ - public function handleFailure($response) - { - $response->getHeaders()->set('WWW-Authenticate', "Bearer realm=\"{$this->realm}\""); - throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); - } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + $response->getHeaders()->set('WWW-Authenticate', "Bearer realm=\"{$this->realm}\""); + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } } diff --git a/framework/rest/IndexAction.php b/framework/rest/IndexAction.php index ca30220a08a..1ca473b1989 100644 --- a/framework/rest/IndexAction.php +++ b/framework/rest/IndexAction.php @@ -16,50 +16,50 @@ */ class IndexAction extends Action { - /** - * @var callable a PHP callable that will be called to prepare a data provider that - * should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead. - * The signature of the callable should be: - * - * ```php - * function ($action) { - * // $action is the action object currently running - * } - * ``` - * - * The callable should return an instance of [[ActiveDataProvider]]. - */ - public $prepareDataProvider; + /** + * @var callable a PHP callable that will be called to prepare a data provider that + * should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead. + * The signature of the callable should be: + * + * ```php + * function ($action) { + * // $action is the action object currently running + * } + * ``` + * + * The callable should return an instance of [[ActiveDataProvider]]. + */ + public $prepareDataProvider; + /** + * @return ActiveDataProvider + */ + public function run() + { + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id); + } - /** - * @return ActiveDataProvider - */ - public function run() - { - if ($this->checkAccess) { - call_user_func($this->checkAccess, $this->id); - } + return $this->prepareDataProvider(); + } - return $this->prepareDataProvider(); - } + /** + * Prepares the data provider that should return the requested collection of the models. + * @return ActiveDataProvider + */ + protected function prepareDataProvider() + { + if ($this->prepareDataProvider !== null) { + return call_user_func($this->prepareDataProvider, $this); + } - /** - * Prepares the data provider that should return the requested collection of the models. - * @return ActiveDataProvider - */ - protected function prepareDataProvider() - { - if ($this->prepareDataProvider !== null) { - return call_user_func($this->prepareDataProvider, $this); - } + /** + * @var \yii\db\BaseActiveRecord $modelClass + */ + $modelClass = $this->modelClass; - /** - * @var \yii\db\BaseActiveRecord $modelClass - */ - $modelClass = $this->modelClass; - return new ActiveDataProvider([ - 'query' => $modelClass::find(), - ]); - } + return new ActiveDataProvider([ + 'query' => $modelClass::find(), + ]); + } } diff --git a/framework/rest/OptionsAction.php b/framework/rest/OptionsAction.php index 0f9561fd75f..ad466c1d045 100644 --- a/framework/rest/OptionsAction.php +++ b/framework/rest/OptionsAction.php @@ -17,26 +17,25 @@ */ class OptionsAction extends \yii\base\Action { - /** - * @var array the HTTP verbs that are supported by the collection URL - */ - public $collectionOptions = ['GET', 'POST', 'HEAD', 'OPTIONS']; - /** - * @var array the HTTP verbs that are supported by the resource URL - */ - public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; + /** + * @var array the HTTP verbs that are supported by the collection URL + */ + public $collectionOptions = ['GET', 'POST', 'HEAD', 'OPTIONS']; + /** + * @var array the HTTP verbs that are supported by the resource URL + */ + public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; - - /** - * Responds to the OPTIONS request. - * @param string $id - */ - public function run($id = null) - { - if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') { - Yii::$app->getResponse()->setStatusCode(405); - } - $options = $id === null ? $this->collectionOptions : $this->resourceOptions; - Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $options)); - } + /** + * Responds to the OPTIONS request. + * @param string $id + */ + public function run($id = null) + { + if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') { + Yii::$app->getResponse()->setStatusCode(405); + } + $options = $id === null ? $this->collectionOptions : $this->resourceOptions; + Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $options)); + } } diff --git a/framework/rest/QueryParamAuth.php b/framework/rest/QueryParamAuth.php index f45e4c8c3ef..ff50c8cdec7 100644 --- a/framework/rest/QueryParamAuth.php +++ b/framework/rest/QueryParamAuth.php @@ -19,34 +19,35 @@ */ class QueryParamAuth extends Component implements AuthInterface { - /** - * @var string the parameter name for passing the access token - */ - public $tokenParam = 'access-token'; + /** + * @var string the parameter name for passing the access token + */ + public $tokenParam = 'access-token'; - /** - * @inheritdoc - */ - public function authenticate($user, $request, $response) - { - $accessToken = $request->get($this->tokenParam); - if (is_string($accessToken)) { - $identity = $user->loginByAccessToken($accessToken); - if ($identity !== null) { - return $identity; - } - } - if ($accessToken !== null) { - $this->handleFailure($response); - } - return null; - } + /** + * @inheritdoc + */ + public function authenticate($user, $request, $response) + { + $accessToken = $request->get($this->tokenParam); + if (is_string($accessToken)) { + $identity = $user->loginByAccessToken($accessToken); + if ($identity !== null) { + return $identity; + } + } + if ($accessToken !== null) { + $this->handleFailure($response); + } - /** - * @inheritdoc - */ - public function handleFailure($response) - { - throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); - } + return null; + } + + /** + * @inheritdoc + */ + public function handleFailure($response) + { + throw new UnauthorizedHttpException('You are requesting with an invalid access token.'); + } } diff --git a/framework/rest/RateLimitInterface.php b/framework/rest/RateLimitInterface.php index 07f60e02704..72150a1c360 100644 --- a/framework/rest/RateLimitInterface.php +++ b/framework/rest/RateLimitInterface.php @@ -15,25 +15,25 @@ */ interface RateLimitInterface { - /** - * Returns the maximum number of allowed requests and the window size. - * @param array $params the additional parameters associated with the rate limit. - * @return array an array of two elements. The first element is the maximum number of allowed requests, - * and the second element is the size of the window in seconds. - */ - public function getRateLimit($params = []); - /** - * Loads the number of allowed requests and the corresponding timestamp from a persistent storage. - * @param array $params the additional parameters associated with the rate limit. - * @return array an array of two elements. The first element is the number of allowed requests, - * and the second element is the corresponding UNIX timestamp. - */ - public function loadAllowance($params = []); - /** - * Saves the number of allowed requests and the corresponding timestamp to a persistent storage. - * @param integer $allowance the number of allowed requests remaining. - * @param integer $timestamp the current timestamp. - * @param array $params the additional parameters associated with the rate limit. - */ - public function saveAllowance($allowance, $timestamp, $params = []); + /** + * Returns the maximum number of allowed requests and the window size. + * @param array $params the additional parameters associated with the rate limit. + * @return array an array of two elements. The first element is the maximum number of allowed requests, + * and the second element is the size of the window in seconds. + */ + public function getRateLimit($params = []); + /** + * Loads the number of allowed requests and the corresponding timestamp from a persistent storage. + * @param array $params the additional parameters associated with the rate limit. + * @return array an array of two elements. The first element is the number of allowed requests, + * and the second element is the corresponding UNIX timestamp. + */ + public function loadAllowance($params = []); + /** + * Saves the number of allowed requests and the corresponding timestamp to a persistent storage. + * @param integer $allowance the number of allowed requests remaining. + * @param integer $timestamp the current timestamp. + * @param array $params the additional parameters associated with the rate limit. + */ + public function saveAllowance($allowance, $timestamp, $params = []); } diff --git a/framework/rest/RateLimiter.php b/framework/rest/RateLimiter.php index 753a0f0c67b..a6b890a5df6 100644 --- a/framework/rest/RateLimiter.php +++ b/framework/rest/RateLimiter.php @@ -23,63 +23,63 @@ */ class RateLimiter extends Component { - /** - * @var boolean whether to include rate limit headers in the response - */ - public $enableRateLimitHeaders = true; - /** - * @var string the message to be displayed when rate limit exceeds - */ - public $errorMessage = 'Rate limit exceeded.'; + /** + * @var boolean whether to include rate limit headers in the response + */ + public $enableRateLimitHeaders = true; + /** + * @var string the message to be displayed when rate limit exceeds + */ + public $errorMessage = 'Rate limit exceeded.'; - /** - * Checks whether the rate limit exceeds. - * @param RateLimitInterface $user the current user - * @param Request $request - * @param Response $response - * @param Action $action the action to be executed - * @throws TooManyRequestsHttpException if rate limit exceeds - */ - public function check($user, $request, $response, $action) - { - $current = time(); - $params = [ - 'request' => $request, - 'action' => $action, - ]; + /** + * Checks whether the rate limit exceeds. + * @param RateLimitInterface $user the current user + * @param Request $request + * @param Response $response + * @param Action $action the action to be executed + * @throws TooManyRequestsHttpException if rate limit exceeds + */ + public function check($user, $request, $response, $action) + { + $current = time(); + $params = [ + 'request' => $request, + 'action' => $action, + ]; - list ($limit, $window) = $user->getRateLimit($params); - list ($allowance, $timestamp) = $user->loadAllowance($params); + list ($limit, $window) = $user->getRateLimit($params); + list ($allowance, $timestamp) = $user->loadAllowance($params); - $allowance += (int)(($current - $timestamp) * $limit / $window); - if ($allowance > $limit) { - $allowance = $limit; - } + $allowance += (int) (($current - $timestamp) * $limit / $window); + if ($allowance > $limit) { + $allowance = $limit; + } - if ($allowance < 1) { - $user->saveAllowance(0, $current, $params); - $this->addRateLimitHeaders($response, $limit, 0, $window); - throw new TooManyRequestsHttpException($this->errorMessage); - } else { - $user->saveAllowance($allowance - 1, $current, $params); - $this->addRateLimitHeaders($response, $limit, 0, (int)(($limit - $allowance) * $window / $limit)); - } - } + if ($allowance < 1) { + $user->saveAllowance(0, $current, $params); + $this->addRateLimitHeaders($response, $limit, 0, $window); + throw new TooManyRequestsHttpException($this->errorMessage); + } else { + $user->saveAllowance($allowance - 1, $current, $params); + $this->addRateLimitHeaders($response, $limit, 0, (int) (($limit - $allowance) * $window / $limit)); + } + } - /** - * Adds the rate limit headers to the response - * @param Response $response - * @param integer $limit the maximum number of allowed requests during a period - * @param integer $remaining the remaining number of allowed requests within the current period - * @param integer $reset the number of seconds to wait before having maximum number of allowed requests again - */ - protected function addRateLimitHeaders($response, $limit, $remaining, $reset) - { - if ($this->enableRateLimitHeaders) { - $response->getHeaders() - ->set('X-Rate-Limit-Limit', $limit) - ->set('X-Rate-Limit-Remaining', $remaining) - ->set('X-Rate-Limit-Reset', $reset); - } - } + /** + * Adds the rate limit headers to the response + * @param Response $response + * @param integer $limit the maximum number of allowed requests during a period + * @param integer $remaining the remaining number of allowed requests within the current period + * @param integer $reset the number of seconds to wait before having maximum number of allowed requests again + */ + protected function addRateLimitHeaders($response, $limit, $remaining, $reset) + { + if ($this->enableRateLimitHeaders) { + $response->getHeaders() + ->set('X-Rate-Limit-Limit', $limit) + ->set('X-Rate-Limit-Remaining', $remaining) + ->set('X-Rate-Limit-Reset', $reset); + } + } } diff --git a/framework/rest/Serializer.php b/framework/rest/Serializer.php index 2a63af06e3c..264adedc214 100644 --- a/framework/rest/Serializer.php +++ b/framework/rest/Serializer.php @@ -31,237 +31,241 @@ */ class Serializer extends Component { - /** - * @var string the name of the query parameter containing the information about which fields should be returned - * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined - * by [[Model::fields()]] will be returned. - */ - public $fieldsParam = 'fields'; - /** - * @var string the name of the query parameter containing the information about which fields should be returned - * in addition to those listed in [[fieldsParam]] for a resource object. - */ - public $expandParam = 'expand'; - /** - * @var string the name of the HTTP header containing the information about total number of data items. - * This is used when serving a resource collection with pagination. - */ - public $totalCountHeader = 'X-Pagination-Total-Count'; - /** - * @var string the name of the HTTP header containing the information about total number of pages of data. - * This is used when serving a resource collection with pagination. - */ - public $pageCountHeader = 'X-Pagination-Page-Count'; - /** - * @var string the name of the HTTP header containing the information about the current page number (1-based). - * This is used when serving a resource collection with pagination. - */ - public $currentPageHeader = 'X-Pagination-Current-Page'; - /** - * @var string the name of the HTTP header containing the information about the number of data items in each page. - * This is used when serving a resource collection with pagination. - */ - public $perPageHeader = 'X-Pagination-Per-Page'; - /** - * @var string the name of the envelope (e.g. `items`) for returning the resource objects in a collection. - * This is used when serving a resource collection. When this is set and pagination is enabled, the serializer - * will return a collection in the following format: - * - * ```php - * [ - * 'items' => [...], // assuming collectionEnvelope is "items" - * '_links' => { // pagination links as returned by Pagination::getLinks() - * 'self' => '...', - * 'next' => '...', - * 'last' => '...', - * }, - * '_meta' => { // meta information as returned by Pagination::toArray() - * 'totalCount' => 100, - * 'pageCount' => 5, - * 'currentPage' => 1, - * 'perPage' => 20, - * }, - * ] - * ``` - * - * If this property is not set, the resource arrays will be directly returned without using envelope. - * The pagination information as shown in `_links` and `_meta` can be accessed from the response HTTP headers. - */ - public $collectionEnvelope; - /** - * @var Request the current request. If not set, the `request` application component will be used. - */ - public $request; - /** - * @var Response the response to be sent. If not set, the `response` application component will be used. - */ - public $response; + /** + * @var string the name of the query parameter containing the information about which fields should be returned + * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined + * by [[Model::fields()]] will be returned. + */ + public $fieldsParam = 'fields'; + /** + * @var string the name of the query parameter containing the information about which fields should be returned + * in addition to those listed in [[fieldsParam]] for a resource object. + */ + public $expandParam = 'expand'; + /** + * @var string the name of the HTTP header containing the information about total number of data items. + * This is used when serving a resource collection with pagination. + */ + public $totalCountHeader = 'X-Pagination-Total-Count'; + /** + * @var string the name of the HTTP header containing the information about total number of pages of data. + * This is used when serving a resource collection with pagination. + */ + public $pageCountHeader = 'X-Pagination-Page-Count'; + /** + * @var string the name of the HTTP header containing the information about the current page number (1-based). + * This is used when serving a resource collection with pagination. + */ + public $currentPageHeader = 'X-Pagination-Current-Page'; + /** + * @var string the name of the HTTP header containing the information about the number of data items in each page. + * This is used when serving a resource collection with pagination. + */ + public $perPageHeader = 'X-Pagination-Per-Page'; + /** + * @var string the name of the envelope (e.g. `items`) for returning the resource objects in a collection. + * This is used when serving a resource collection. When this is set and pagination is enabled, the serializer + * will return a collection in the following format: + * + * ```php + * [ + * 'items' => [...], // assuming collectionEnvelope is "items" + * '_links' => { // pagination links as returned by Pagination::getLinks() + * 'self' => '...', + * 'next' => '...', + * 'last' => '...', + * }, + * '_meta' => { // meta information as returned by Pagination::toArray() + * 'totalCount' => 100, + * 'pageCount' => 5, + * 'currentPage' => 1, + * 'perPage' => 20, + * }, + * ] + * ``` + * + * If this property is not set, the resource arrays will be directly returned without using envelope. + * The pagination information as shown in `_links` and `_meta` can be accessed from the response HTTP headers. + */ + public $collectionEnvelope; + /** + * @var Request the current request. If not set, the `request` application component will be used. + */ + public $request; + /** + * @var Response the response to be sent. If not set, the `response` application component will be used. + */ + public $response; - /** - * @inheritdoc - */ - public function init() - { - if ($this->request === null) { - $this->request = Yii::$app->getRequest(); - } - if ($this->response === null) { - $this->response = Yii::$app->getResponse(); - } - } + /** + * @inheritdoc + */ + public function init() + { + if ($this->request === null) { + $this->request = Yii::$app->getRequest(); + } + if ($this->response === null) { + $this->response = Yii::$app->getResponse(); + } + } - /** - * Serializes the given data into a format that can be easily turned into other formats. - * This method mainly converts the objects of recognized types into array representation. - * It will not do conversion for unknown object types or non-object data. - * The default implementation will handle [[Model]] and [[DataProviderInterface]]. - * You may override this method to support more object types. - * @param mixed $data the data to be serialized. - * @return mixed the converted data. - */ - public function serialize($data) - { - if ($data instanceof Model) { - return $data->hasErrors() ? $this->serializeModelErrors($data) : $this->serializeModel($data); - } elseif ($data instanceof DataProviderInterface) { - return $this->serializeDataProvider($data); - } else { - return $data; - } - } + /** + * Serializes the given data into a format that can be easily turned into other formats. + * This method mainly converts the objects of recognized types into array representation. + * It will not do conversion for unknown object types or non-object data. + * The default implementation will handle [[Model]] and [[DataProviderInterface]]. + * You may override this method to support more object types. + * @param mixed $data the data to be serialized. + * @return mixed the converted data. + */ + public function serialize($data) + { + if ($data instanceof Model) { + return $data->hasErrors() ? $this->serializeModelErrors($data) : $this->serializeModel($data); + } elseif ($data instanceof DataProviderInterface) { + return $this->serializeDataProvider($data); + } else { + return $data; + } + } - /** - * @return array the names of the requested fields. The first element is an array - * representing the list of default fields requested, while the second element is - * an array of the extra fields requested in addition to the default fields. - * @see Model::fields() - * @see Model::extraFields() - */ - protected function getRequestedFields() - { - $fields = $this->request->get($this->fieldsParam); - $expand = $this->request->get($this->expandParam); - return [ - preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY), - preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY), - ]; - } + /** + * @return array the names of the requested fields. The first element is an array + * representing the list of default fields requested, while the second element is + * an array of the extra fields requested in addition to the default fields. + * @see Model::fields() + * @see Model::extraFields() + */ + protected function getRequestedFields() + { + $fields = $this->request->get($this->fieldsParam); + $expand = $this->request->get($this->expandParam); - /** - * Serializes a data provider. - * @param DataProviderInterface $dataProvider - * @return array the array representation of the data provider. - */ - protected function serializeDataProvider($dataProvider) - { - $models = $this->serializeModels($dataProvider->getModels()); + return [ + preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY), + preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY), + ]; + } - if (($pagination = $dataProvider->getPagination()) !== false) { - $this->addPaginationHeaders($pagination); - } + /** + * Serializes a data provider. + * @param DataProviderInterface $dataProvider + * @return array the array representation of the data provider. + */ + protected function serializeDataProvider($dataProvider) + { + $models = $this->serializeModels($dataProvider->getModels()); - if ($this->request->getIsHead()) { - return null; - } elseif ($this->collectionEnvelope === null) { - return $models; - } else { - $result = [ - $this->collectionEnvelope => $models, - ]; - if ($pagination !== false) { - return array_merge($result, $this->serializePagination($pagination)); - } else { - return $result; - } - } - } + if (($pagination = $dataProvider->getPagination()) !== false) { + $this->addPaginationHeaders($pagination); + } - /** - * Serializes a pagination into an array. - * @param Pagination $pagination - * @return array the array representation of the pagination - * @see addPaginationHeader() - */ - protected function serializePagination($pagination) - { - return [ - '_links' => Link::serialize($pagination->getLinks(true)), - '_meta' => [ - 'totalCount' => $pagination->totalCount, - 'pageCount' => $pagination->getPageCount(), - 'currentPage' => $pagination->getPage(), - 'perPage' => $pagination->getPageSize(), - ], - ]; - } + if ($this->request->getIsHead()) { + return null; + } elseif ($this->collectionEnvelope === null) { + return $models; + } else { + $result = [ + $this->collectionEnvelope => $models, + ]; + if ($pagination !== false) { + return array_merge($result, $this->serializePagination($pagination)); + } else { + return $result; + } + } + } - /** - * Adds HTTP headers about the pagination to the response. - * @param Pagination $pagination - */ - protected function addPaginationHeaders($pagination) - { - $links = []; - foreach ($pagination->getLinks(true) as $rel => $url) { - $links[] = "<$url>; rel=$rel"; - } + /** + * Serializes a pagination into an array. + * @param Pagination $pagination + * @return array the array representation of the pagination + * @see addPaginationHeader() + */ + protected function serializePagination($pagination) + { + return [ + '_links' => Link::serialize($pagination->getLinks(true)), + '_meta' => [ + 'totalCount' => $pagination->totalCount, + 'pageCount' => $pagination->getPageCount(), + 'currentPage' => $pagination->getPage(), + 'perPage' => $pagination->getPageSize(), + ], + ]; + } - $this->response->getHeaders() - ->set($this->totalCountHeader, $pagination->totalCount) - ->set($this->pageCountHeader, $pagination->getPageCount()) - ->set($this->currentPageHeader, $pagination->getPage() + 1) - ->set($this->perPageHeader, $pagination->pageSize) - ->set('Link', implode(', ', $links)); - } + /** + * Adds HTTP headers about the pagination to the response. + * @param Pagination $pagination + */ + protected function addPaginationHeaders($pagination) + { + $links = []; + foreach ($pagination->getLinks(true) as $rel => $url) { + $links[] = "<$url>; rel=$rel"; + } - /** - * Serializes a model object. - * @param Model $model - * @return array the array representation of the model - */ - protected function serializeModel($model) - { - if ($this->request->getIsHead()) { - return null; - } else { - list ($fields, $expand) = $this->getRequestedFields(); - return $model->toArray($fields, $expand); - } - } + $this->response->getHeaders() + ->set($this->totalCountHeader, $pagination->totalCount) + ->set($this->pageCountHeader, $pagination->getPageCount()) + ->set($this->currentPageHeader, $pagination->getPage() + 1) + ->set($this->perPageHeader, $pagination->pageSize) + ->set('Link', implode(', ', $links)); + } - /** - * Serializes the validation errors in a model. - * @param Model $model - * @return array the array representation of the errors - */ - protected function serializeModelErrors($model) - { - $this->response->setStatusCode(422, 'Data Validation Failed.'); - $result = []; - foreach ($model->getFirstErrors() as $name => $message) { - $result[] = [ - 'field' => $name, - 'message' => $message, - ]; - } - return $result; - } + /** + * Serializes a model object. + * @param Model $model + * @return array the array representation of the model + */ + protected function serializeModel($model) + { + if ($this->request->getIsHead()) { + return null; + } else { + list ($fields, $expand) = $this->getRequestedFields(); - /** - * Serializes a set of models. - * @param array $models - * @return array the array representation of the models - */ - protected function serializeModels(array $models) - { - list ($fields, $expand) = $this->getRequestedFields(); - foreach ($models as $i => $model) { - if ($model instanceof Model) { - $models[$i] = $model->toArray($fields, $expand); - } elseif (is_array($model)) { - $models[$i] = ArrayHelper::toArray($model); - } - } - return $models; - } + return $model->toArray($fields, $expand); + } + } + + /** + * Serializes the validation errors in a model. + * @param Model $model + * @return array the array representation of the errors + */ + protected function serializeModelErrors($model) + { + $this->response->setStatusCode(422, 'Data Validation Failed.'); + $result = []; + foreach ($model->getFirstErrors() as $name => $message) { + $result[] = [ + 'field' => $name, + 'message' => $message, + ]; + } + + return $result; + } + + /** + * Serializes a set of models. + * @param array $models + * @return array the array representation of the models + */ + protected function serializeModels(array $models) + { + list ($fields, $expand) = $this->getRequestedFields(); + foreach ($models as $i => $model) { + if ($model instanceof Model) { + $models[$i] = $model->toArray($fields, $expand); + } elseif (is_array($model)) { + $models[$i] = ArrayHelper::toArray($model); + } + } + + return $models; + } } diff --git a/framework/rest/UpdateAction.php b/framework/rest/UpdateAction.php index 7a14a0a544b..969b076fb8c 100644 --- a/framework/rest/UpdateAction.php +++ b/framework/rest/UpdateAction.php @@ -19,49 +19,48 @@ */ class UpdateAction extends Action { - /** - * @var string the scenario to be assigned to the model before it is validated and updated. - */ - public $scenario = Model::SCENARIO_DEFAULT; - /** - * @var boolean whether to start a DB transaction when saving the model. - */ - public $transactional = true; + /** + * @var string the scenario to be assigned to the model before it is validated and updated. + */ + public $scenario = Model::SCENARIO_DEFAULT; + /** + * @var boolean whether to start a DB transaction when saving the model. + */ + public $transactional = true; + /** + * Updates an existing model. + * @param string $id the primary key of the model. + * @return \yii\db\ActiveRecordInterface the model being updated + * @throws \Exception if there is any error when updating the model + */ + public function run($id) + { + /** @var ActiveRecord $model */ + $model = $this->findModel($id); - /** - * Updates an existing model. - * @param string $id the primary key of the model. - * @return \yii\db\ActiveRecordInterface the model being updated - * @throws \Exception if there is any error when updating the model - */ - public function run($id) - { - /** @var ActiveRecord $model */ - $model = $this->findModel($id); + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } - if ($this->checkAccess) { - call_user_func($this->checkAccess, $this->id, $model); - } + $model->scenario = $this->scenario; + $model->load(Yii::$app->getRequest()->getBodyParams(), ''); - $model->scenario = $this->scenario; - $model->load(Yii::$app->getRequest()->getBodyParams(), ''); + if ($this->transactional && $model instanceof ActiveRecord) { + if ($model->validate()) { + $transaction = $model->getDb()->beginTransaction(); + try { + $model->update(false); + $transaction->commit(); + } catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + } + } else { + $model->save(); + } - if ($this->transactional && $model instanceof ActiveRecord) { - if ($model->validate()) { - $transaction = $model->getDb()->beginTransaction(); - try { - $model->update(false); - $transaction->commit(); - } catch (\Exception $e) { - $transaction->rollback(); - throw $e; - } - } - } else { - $model->save(); - } - - return $model; - } + return $model; + } } diff --git a/framework/rest/UrlRule.php b/framework/rest/UrlRule.php index 5e4b218569e..c63ff6e6548 100644 --- a/framework/rest/UrlRule.php +++ b/framework/rest/UrlRule.php @@ -59,192 +59,195 @@ */ class UrlRule extends CompositeUrlRule { - /** - * @var string the common prefix string shared by all patterns. - */ - public $prefix; - /** - * @var string the suffix that will be assigned to [[\yii\web\UrlRule::suffix]] for every generated rule. - */ - public $suffix; - /** - * @var string|array the controller ID (e.g. `user`, `post-comment`) that the rules in this composite rule - * are dealing with. It should be prefixed with the module ID if the controller is within a module (e.g. `admin/user`). - * - * By default, the controller ID will be pluralized automatically when it is put in the patterns of the - * generated rules. If you want to explicitly specify how the controller ID should appear in the patterns, - * you may use an array with the array key being as the controller ID in the pattern, and the array value - * the actual controller ID. For example, `['u' => 'user']`. - * - * You may also pass multiple controller IDs as an array. If this is the case, this composite rule will - * generate applicable URL rules for EVERY specified controller. For example, `['user', 'post']`. - */ - public $controller; - /** - * @var array list of acceptable actions. If not empty, only the actions within this array - * will have the corresponding URL rules created. - * @see patterns - */ - public $only = []; - /** - * @var array list of actions that should be excluded. Any action found in this array - * will NOT have its URL rules created. - * @see patterns - */ - public $except = []; - /** - * @var array patterns for supporting extra actions in addition to those listed in [[patterns]]. - * The keys are the patterns and the values are the corresponding action IDs. - * These extra patterns will take precedence over [[patterns]]. - */ - public $extraPatterns = []; - /** - * @var array list of tokens that should be replaced for each pattern. The keys are the token names, - * and the values are the corresponding replacements. - * @see patterns - */ - public $tokens = [ - '{id}' => '', - ]; - /** - * @var array list of possible patterns and the corresponding actions for creating the URL rules. - * The keys are the patterns and the values are the corresponding actions. - * The format of patterns is `Verbs Pattern`, where `Verbs` stands for a list of HTTP verbs separated - * by comma (without space). If `Verbs` is not specified, it means all verbs are allowed. - * `Pattern` is optional. It will be prefixed with [[prefix]]/[[controller]]/, - * and tokens in it will be replaced by [[tokens]]. - */ - public $patterns = [ - 'PUT,PATCH {id}' => 'update', - 'DELETE {id}' => 'delete', - 'GET,HEAD {id}' => 'view', - 'POST' => 'create', - 'GET,HEAD' => 'index', - '{id}' => 'options', - '' => 'options', - ]; - /** - * @var array the default configuration for creating each URL rule contained by this rule. - */ - public $ruleConfig = [ - 'class' => 'yii\web\UrlRule', - ]; - /** - * @var boolean whether to automatically pluralize the URL names for controllers. - * If true, a controller ID will appear in plural form in URLs. For example, `user` controller - * will appear as `users` in URLs. - * @see controllers - */ - public $pluralize = true; - - - /** - * @inheritdoc - */ - public function init() - { - if (empty($this->controller)) { - throw new InvalidConfigException('"controller" must be set.'); - } - - $controllers = []; - foreach ((array)$this->controller as $urlName => $controller) { - if (is_integer($urlName)) { - $urlName = $this->pluralize ? Inflector::pluralize($controller) : $controller; - } - $controllers[$urlName] = $controller; - } - $this->controller = $controllers; - - $this->prefix = trim($this->prefix, '/'); - - parent::init(); - } - - /** - * @inheritdoc - */ - protected function createRules() - { - $only = array_flip($this->only); - $except = array_flip($this->except); - $patterns = array_merge($this->patterns, $this->extraPatterns); - $rules = []; - foreach ($this->controller as $urlName => $controller) { - $prefix = trim($this->prefix . '/' . $urlName, '/'); - foreach ($patterns as $pattern => $action) { - if (!isset($except[$action]) && (empty($only) || isset($only[$action]))) { - $rules[$urlName][] = $this->createRule($pattern, $prefix, $controller . '/' . $action); - } - } - } - return $rules; - } - - /** - * Creates a URL rule using the given pattern and action. - * @param string $pattern - * @param string $prefix - * @param string $action - * @return \yii\web\UrlRuleInterface - */ - protected function createRule($pattern, $prefix, $action) - { - $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS'; - if (preg_match("/^((?:($verbs),)*($verbs))(?:\\s+(.*))?$/", $pattern, $matches)) { - $verbs = explode(',', $matches[1]); - $pattern = isset($matches[4]) ? $matches[4] : ''; - } else { - $verbs = []; - } - - $config = $this->ruleConfig; - $config['verb'] = $verbs; - $config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/'); - $config['route'] = $action; - if (!in_array('GET', $verbs)) { - $config['mode'] = \yii\web\UrlRule::PARSING_ONLY; - } - $config['suffix'] = $this->suffix; - - return Yii::createObject($config); - } - - /** - * @inheritdoc - */ - public function parseRequest($manager, $request) - { - $pathInfo = $request->getPathInfo(); - foreach ($this->rules as $urlName => $rules) { - if (strpos($pathInfo, $urlName) !== false) { - foreach ($rules as $rule) { - /** @var \yii\web\UrlRule $rule */ - if (($result = $rule->parseRequest($manager, $request)) !== false) { - Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); - return $result; - } - } - } - } - return false; - } - - /** - * @inheritdoc - */ - public function createUrl($manager, $route, $params) - { - foreach ($this->controller as $urlName => $controller) { - if (strpos($route, $controller) !== false) { - foreach ($this->rules[$urlName] as $rule) { - /** @var \yii\web\UrlRule $rule */ - if (($url = $rule->createUrl($manager, $route, $params)) !== false) { - return $url; - } - } - } - } - return false; - } + /** + * @var string the common prefix string shared by all patterns. + */ + public $prefix; + /** + * @var string the suffix that will be assigned to [[\yii\web\UrlRule::suffix]] for every generated rule. + */ + public $suffix; + /** + * @var string|array the controller ID (e.g. `user`, `post-comment`) that the rules in this composite rule + * are dealing with. It should be prefixed with the module ID if the controller is within a module (e.g. `admin/user`). + * + * By default, the controller ID will be pluralized automatically when it is put in the patterns of the + * generated rules. If you want to explicitly specify how the controller ID should appear in the patterns, + * you may use an array with the array key being as the controller ID in the pattern, and the array value + * the actual controller ID. For example, `['u' => 'user']`. + * + * You may also pass multiple controller IDs as an array. If this is the case, this composite rule will + * generate applicable URL rules for EVERY specified controller. For example, `['user', 'post']`. + */ + public $controller; + /** + * @var array list of acceptable actions. If not empty, only the actions within this array + * will have the corresponding URL rules created. + * @see patterns + */ + public $only = []; + /** + * @var array list of actions that should be excluded. Any action found in this array + * will NOT have its URL rules created. + * @see patterns + */ + public $except = []; + /** + * @var array patterns for supporting extra actions in addition to those listed in [[patterns]]. + * The keys are the patterns and the values are the corresponding action IDs. + * These extra patterns will take precedence over [[patterns]]. + */ + public $extraPatterns = []; + /** + * @var array list of tokens that should be replaced for each pattern. The keys are the token names, + * and the values are the corresponding replacements. + * @see patterns + */ + public $tokens = [ + '{id}' => '', + ]; + /** + * @var array list of possible patterns and the corresponding actions for creating the URL rules. + * The keys are the patterns and the values are the corresponding actions. + * The format of patterns is `Verbs Pattern`, where `Verbs` stands for a list of HTTP verbs separated + * by comma (without space). If `Verbs` is not specified, it means all verbs are allowed. + * `Pattern` is optional. It will be prefixed with [[prefix]]/[[controller]]/, + * and tokens in it will be replaced by [[tokens]]. + */ + public $patterns = [ + 'PUT,PATCH {id}' => 'update', + 'DELETE {id}' => 'delete', + 'GET,HEAD {id}' => 'view', + 'POST' => 'create', + 'GET,HEAD' => 'index', + '{id}' => 'options', + '' => 'options', + ]; + /** + * @var array the default configuration for creating each URL rule contained by this rule. + */ + public $ruleConfig = [ + 'class' => 'yii\web\UrlRule', + ]; + /** + * @var boolean whether to automatically pluralize the URL names for controllers. + * If true, a controller ID will appear in plural form in URLs. For example, `user` controller + * will appear as `users` in URLs. + * @see controllers + */ + public $pluralize = true; + + /** + * @inheritdoc + */ + public function init() + { + if (empty($this->controller)) { + throw new InvalidConfigException('"controller" must be set.'); + } + + $controllers = []; + foreach ((array) $this->controller as $urlName => $controller) { + if (is_integer($urlName)) { + $urlName = $this->pluralize ? Inflector::pluralize($controller) : $controller; + } + $controllers[$urlName] = $controller; + } + $this->controller = $controllers; + + $this->prefix = trim($this->prefix, '/'); + + parent::init(); + } + + /** + * @inheritdoc + */ + protected function createRules() + { + $only = array_flip($this->only); + $except = array_flip($this->except); + $patterns = array_merge($this->patterns, $this->extraPatterns); + $rules = []; + foreach ($this->controller as $urlName => $controller) { + $prefix = trim($this->prefix . '/' . $urlName, '/'); + foreach ($patterns as $pattern => $action) { + if (!isset($except[$action]) && (empty($only) || isset($only[$action]))) { + $rules[$urlName][] = $this->createRule($pattern, $prefix, $controller . '/' . $action); + } + } + } + + return $rules; + } + + /** + * Creates a URL rule using the given pattern and action. + * @param string $pattern + * @param string $prefix + * @param string $action + * @return \yii\web\UrlRuleInterface + */ + protected function createRule($pattern, $prefix, $action) + { + $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS'; + if (preg_match("/^((?:($verbs),)*($verbs))(?:\\s+(.*))?$/", $pattern, $matches)) { + $verbs = explode(',', $matches[1]); + $pattern = isset($matches[4]) ? $matches[4] : ''; + } else { + $verbs = []; + } + + $config = $this->ruleConfig; + $config['verb'] = $verbs; + $config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/'); + $config['route'] = $action; + if (!in_array('GET', $verbs)) { + $config['mode'] = \yii\web\UrlRule::PARSING_ONLY; + } + $config['suffix'] = $this->suffix; + + return Yii::createObject($config); + } + + /** + * @inheritdoc + */ + public function parseRequest($manager, $request) + { + $pathInfo = $request->getPathInfo(); + foreach ($this->rules as $urlName => $rules) { + if (strpos($pathInfo, $urlName) !== false) { + foreach ($rules as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($result = $rule->parseRequest($manager, $request)) !== false) { + Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); + + return $result; + } + } + } + } + + return false; + } + + /** + * @inheritdoc + */ + public function createUrl($manager, $route, $params) + { + foreach ($this->controller as $urlName => $controller) { + if (strpos($route, $controller) !== false) { + foreach ($this->rules[$urlName] as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($url = $rule->createUrl($manager, $route, $params)) !== false) { + return $url; + } + } + } + } + + return false; + } } diff --git a/framework/rest/ViewAction.php b/framework/rest/ViewAction.php index c37522f61ed..daf86570e12 100644 --- a/framework/rest/ViewAction.php +++ b/framework/rest/ViewAction.php @@ -17,17 +17,18 @@ */ class ViewAction extends Action { - /** - * Displays a model. - * @param string $id the primary key of the model. - * @return \yii\db\ActiveRecordInterface the model being displayed - */ - public function run($id) - { - $model = $this->findModel($id); - if ($this->checkAccess) { - call_user_func($this->checkAccess, $this->id, $model); - } - return $model; - } + /** + * Displays a model. + * @param string $id the primary key of the model. + * @return \yii\db\ActiveRecordInterface the model being displayed + */ + public function run($id) + { + $model = $this->findModel($id); + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } + + return $model; + } } diff --git a/framework/test/ActiveFixture.php b/framework/test/ActiveFixture.php index 6cb2f8ffa78..ee06eaf01c1 100644 --- a/framework/test/ActiveFixture.php +++ b/framework/test/ActiveFixture.php @@ -8,7 +8,6 @@ namespace yii\test; use Yii; -use yii\base\ArrayAccessTrait; use yii\base\InvalidConfigException; use yii\db\TableSchema; @@ -32,132 +31,132 @@ */ class ActiveFixture extends BaseActiveFixture { - /** - * @var string the name of the database table that this fixture is about. If this property is not set, - * the table name will be determined via [[modelClass]]. - * @see modelClass - */ - public $tableName; - /** - * @var string|boolean the file path or path alias of the data file that contains the fixture data - * to be returned by [[getData()]]. If this is not set, it will default to `FixturePath/data/TableName.php`, - * where `FixturePath` stands for the directory containing this fixture class, and `TableName` stands for the - * name of the table associated with this fixture. You can set this property to be false to prevent loading any data. - */ - public $dataFile; - /** - * @var TableSchema the table schema for the table associated with this fixture - */ - private $_table; + /** + * @var string the name of the database table that this fixture is about. If this property is not set, + * the table name will be determined via [[modelClass]]. + * @see modelClass + */ + public $tableName; + /** + * @var string|boolean the file path or path alias of the data file that contains the fixture data + * to be returned by [[getData()]]. If this is not set, it will default to `FixturePath/data/TableName.php`, + * where `FixturePath` stands for the directory containing this fixture class, and `TableName` stands for the + * name of the table associated with this fixture. You can set this property to be false to prevent loading any data. + */ + public $dataFile; + /** + * @var TableSchema the table schema for the table associated with this fixture + */ + private $_table; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!isset($this->modelClass) && !isset($this->tableName)) { + throw new InvalidConfigException('Either "modelClass" or "tableName" must be set.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (!isset($this->modelClass) && !isset($this->tableName)) { - throw new InvalidConfigException('Either "modelClass" or "tableName" must be set.'); - } - } + /** + * Loads the fixture. + * + * It will then populate the table with the data returned by [[getData()]]. + * + * If you override this method, you should consider calling the parent implementation + * so that the data returned by [[getData()]] can be populated into the table. + */ + public function load() + { + parent::load(); - /** - * Loads the fixture. - * - * It will then populate the table with the data returned by [[getData()]]. - * - * If you override this method, you should consider calling the parent implementation - * so that the data returned by [[getData()]] can be populated into the table. - */ - public function load() - { - parent::load(); + $table = $this->getTableSchema(); - $table = $this->getTableSchema(); + foreach ($this->getData() as $alias => $row) { + $this->db->createCommand()->insert($table->fullName, $row)->execute(); + if ($table->sequenceName !== null) { + foreach ($table->primaryKey as $pk) { + if (!isset($row[$pk])) { + $row[$pk] = $this->db->getLastInsertID($table->sequenceName); + break; + } + } + } + $this->data[$alias] = $row; + } + } - foreach ($this->getData() as $alias => $row) { - $this->db->createCommand()->insert($table->fullName, $row)->execute(); - if ($table->sequenceName !== null) { - foreach ($table->primaryKey as $pk) { - if (!isset($row[$pk])) { - $row[$pk] = $this->db->getLastInsertID($table->sequenceName); - break; - } - } - } - $this->data[$alias] = $row; - } - } + /** + * Unloads the fixture. + * + * The default implementation will clean up the table by calling [[resetTable()]]. + */ + public function unload() + { + $this->resetTable(); + parent::unload(); + } - /** - * Unloads the fixture. - * - * The default implementation will clean up the table by calling [[resetTable()]]. - */ - public function unload() - { - $this->resetTable(); - parent::unload(); - } + /** + * Returns the fixture data. + * + * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. + * The file should return an array of data rows (column name => column value), each corresponding to a row in the table. + * + * If the data file does not exist, an empty array will be returned. + * + * @return array the data rows to be inserted into the database table. + */ + protected function getData() + { + if ($this->dataFile === null) { + $class = new \ReflectionClass($this); + $dataFile = dirname($class->getFileName()) . '/data/' . $this->getTableSchema()->fullName . '.php'; - /** - * Returns the fixture data. - * - * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. - * The file should return an array of data rows (column name => column value), each corresponding to a row in the table. - * - * If the data file does not exist, an empty array will be returned. - * - * @return array the data rows to be inserted into the database table. - */ - protected function getData() - { - if ($this->dataFile === null) { - $class = new \ReflectionClass($this); - $dataFile = dirname($class->getFileName()) . '/data/' . $this->getTableSchema()->fullName . '.php'; - return is_file($dataFile) ? require($dataFile) : []; - } else { - return parent::getData(); - } - } + return is_file($dataFile) ? require($dataFile) : []; + } else { + return parent::getData(); + } + } - /** - * Removes all existing data from the specified table and resets sequence number to 1 (if any). - * This method is called before populating fixture data into the table associated with this fixture. - */ - protected function resetTable() - { - $table = $this->getTableSchema(); - $this->db->createCommand()->delete($table->fullName)->execute(); - if ($table->sequenceName !== null) { - $this->db->createCommand()->resetSequence($table->fullName, 1)->execute(); - } - } + /** + * Removes all existing data from the specified table and resets sequence number to 1 (if any). + * This method is called before populating fixture data into the table associated with this fixture. + */ + protected function resetTable() + { + $table = $this->getTableSchema(); + $this->db->createCommand()->delete($table->fullName)->execute(); + if ($table->sequenceName !== null) { + $this->db->createCommand()->resetSequence($table->fullName, 1)->execute(); + } + } - /** - * @return TableSchema the schema information of the database table associated with this fixture. - * @throws \yii\base\InvalidConfigException if the table does not exist - */ - public function getTableSchema() - { - if ($this->_table !== null) { - return $this->_table; - } + /** + * @return TableSchema the schema information of the database table associated with this fixture. + * @throws \yii\base\InvalidConfigException if the table does not exist + */ + public function getTableSchema() + { + if ($this->_table !== null) { + return $this->_table; + } - $db = $this->db; - $tableName = $this->tableName; - if ($tableName === null) { - /** @var \yii\db\ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - $tableName = $modelClass::tableName(); - } + $db = $this->db; + $tableName = $this->tableName; + if ($tableName === null) { + /** @var \yii\db\ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + $tableName = $modelClass::tableName(); + } - $this->_table = $db->getSchema()->getTableSchema($tableName); - if ($this->_table === null) { - throw new InvalidConfigException("Table does not exist: {$tableName}"); - } + $this->_table = $db->getSchema()->getTableSchema($tableName); + if ($this->_table === null) { + throw new InvalidConfigException("Table does not exist: {$tableName}"); + } - return $this->_table; - } + return $this->_table; + } } diff --git a/framework/test/BaseActiveFixture.php b/framework/test/BaseActiveFixture.php index 7300bdfbb43..f82fefb7e01 100644 --- a/framework/test/BaseActiveFixture.php +++ b/framework/test/BaseActiveFixture.php @@ -19,88 +19,88 @@ */ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate, \ArrayAccess, \Countable { - use ArrayAccessTrait; + use ArrayAccessTrait; - /** - * @var string the AR model class associated with this fixture. - */ - public $modelClass; - /** - * @var array the data rows. Each array element represents one row of data (column name => column value). - */ - public $data = []; - /** - * @var string|boolean the file path or path alias of the data file that contains the fixture data - * to be returned by [[getData()]]. You can set this property to be false to prevent loading any data. - */ - public $dataFile; - /** - * @var \yii\db\ActiveRecord[] the loaded AR models - */ - private $_models = []; + /** + * @var string the AR model class associated with this fixture. + */ + public $modelClass; + /** + * @var array the data rows. Each array element represents one row of data (column name => column value). + */ + public $data = []; + /** + * @var string|boolean the file path or path alias of the data file that contains the fixture data + * to be returned by [[getData()]]. You can set this property to be false to prevent loading any data. + */ + public $dataFile; + /** + * @var \yii\db\ActiveRecord[] the loaded AR models + */ + private $_models = []; + /** + * Returns the AR model by the specified model name. + * A model name is the key of the corresponding data row in [[data]]. + * @param string $name the model name. + * @return null|\yii\db\ActiveRecord the AR model, or null if the model cannot be found in the database + * @throws \yii\base\InvalidConfigException if [[modelClass]] is not set. + */ + public function getModel($name) + { + if (!isset($this->data[$name])) { + return null; + } + if (array_key_exists($name, $this->_models)) { + return $this->_models[$name]; + } - /** - * Returns the AR model by the specified model name. - * A model name is the key of the corresponding data row in [[data]]. - * @param string $name the model name. - * @return null|\yii\db\ActiveRecord the AR model, or null if the model cannot be found in the database - * @throws \yii\base\InvalidConfigException if [[modelClass]] is not set. - */ - public function getModel($name) - { - if (!isset($this->data[$name])) { - return null; - } - if (array_key_exists($name, $this->_models)) { - return $this->_models[$name]; - } + if ($this->modelClass === null) { + throw new InvalidConfigException('The "modelClass" property must be set.'); + } + $row = $this->data[$name]; + /** @var \yii\db\ActiveRecord $modelClass */ + $modelClass = $this->modelClass; + /** @var \yii\db\ActiveRecord $model */ + $model = new $modelClass; + $keys = []; + foreach ($model->primaryKey() as $key) { + $keys[$key] = isset($row[$key]) ? $row[$key] : null; + } - if ($this->modelClass === null) { - throw new InvalidConfigException('The "modelClass" property must be set.'); - } - $row = $this->data[$name]; - /** @var \yii\db\ActiveRecord $modelClass */ - $modelClass = $this->modelClass; - /** @var \yii\db\ActiveRecord $model */ - $model = new $modelClass; - $keys = []; - foreach ($model->primaryKey() as $key) { - $keys[$key] = isset($row[$key]) ? $row[$key] : null; - } - return $this->_models[$name] = $modelClass::find($keys); - } + return $this->_models[$name] = $modelClass::find($keys); + } - /** - * Loads the fixture. - * - * The default implementation simply stores the data returned by [[getData()]] in [[data]]. - * You should usually override this method by putting the data into the underlying database. - */ - public function load() - { - $this->data = $this->getData(); - } + /** + * Loads the fixture. + * + * The default implementation simply stores the data returned by [[getData()]] in [[data]]. + * You should usually override this method by putting the data into the underlying database. + */ + public function load() + { + $this->data = $this->getData(); + } - /** - * Returns the fixture data. - * - * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. - * The file should return the data array that will be stored in [[data]] after inserting into the database. - * - * @return array the data to be put into the database - * @throws InvalidConfigException if the specified data file does not exist. - */ - protected function getData() - { - if ($this->dataFile === false || $this->dataFile === null) { - return []; - } - $dataFile = Yii::getAlias($this->dataFile); - if (is_file($dataFile)) { - return require($dataFile); - } else { - throw new InvalidConfigException("Fixture data file does not exist: {$this->dataFile}"); - } - } + /** + * Returns the fixture data. + * + * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. + * The file should return the data array that will be stored in [[data]] after inserting into the database. + * + * @return array the data to be put into the database + * @throws InvalidConfigException if the specified data file does not exist. + */ + protected function getData() + { + if ($this->dataFile === false || $this->dataFile === null) { + return []; + } + $dataFile = Yii::getAlias($this->dataFile); + if (is_file($dataFile)) { + return require($dataFile); + } else { + throw new InvalidConfigException("Fixture data file does not exist: {$this->dataFile}"); + } + } } diff --git a/framework/test/DbFixture.php b/framework/test/DbFixture.php index 2b5e6cc4928..2b165490597 100644 --- a/framework/test/DbFixture.php +++ b/framework/test/DbFixture.php @@ -21,25 +21,24 @@ */ abstract class DbFixture extends Fixture { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbFixture object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbFixture object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!is_object($this->db)) { - throw new InvalidConfigException("The 'db' property must be either a DB connection instance or the application component ID of a DB connection."); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!is_object($this->db)) { + throw new InvalidConfigException("The 'db' property must be either a DB connection instance or the application component ID of a DB connection."); + } + } } diff --git a/framework/test/Fixture.php b/framework/test/Fixture.php index e22a139e68f..e7a41574d92 100644 --- a/framework/test/Fixture.php +++ b/framework/test/Fixture.php @@ -29,56 +29,55 @@ */ class Fixture extends Component { - /** - * @var array the fixtures that this fixture depends on. This must be a list of the dependent - * fixture class names. - */ - public $depends = []; + /** + * @var array the fixtures that this fixture depends on. This must be a list of the dependent + * fixture class names. + */ + public $depends = []; + /** + * Loads the fixture. + * This method is called before performing every test method. + * You should override this method with concrete implementation about how to set up the fixture. + */ + public function load() + { + } - /** - * Loads the fixture. - * This method is called before performing every test method. - * You should override this method with concrete implementation about how to set up the fixture. - */ - public function load() - { - } + /** + * This method is called BEFORE any fixture data is loaded for the current test. + */ + public function beforeLoad() + { + } - /** - * This method is called BEFORE any fixture data is loaded for the current test. - */ - public function beforeLoad() - { - } + /** + * This method is called AFTER all fixture data have been loaded for the current test. + */ + public function afterLoad() + { + } - /** - * This method is called AFTER all fixture data have been loaded for the current test. - */ - public function afterLoad() - { - } + /** + * Unloads the fixture. + * This method is called after every test method finishes. + * You may override this method to perform necessary cleanup work for the fixture. + */ + public function unload() + { + } - /** - * Unloads the fixture. - * This method is called after every test method finishes. - * You may override this method to perform necessary cleanup work for the fixture. - */ - public function unload() - { - } + /** + * This method is called BEFORE any fixture data is unloaded for the current test. + */ + public function beforeUnload() + { + } - /** - * This method is called BEFORE any fixture data is unloaded for the current test. - */ - public function beforeUnload() - { - } - - /** - * This method is called AFTER all fixture data have been unloaded for the current test. - */ - public function afterUnload() - { - } + /** + * This method is called AFTER all fixture data have been unloaded for the current test. + */ + public function afterUnload() + { + } } diff --git a/framework/test/FixtureTrait.php b/framework/test/FixtureTrait.php index f82c56d86f9..5a87e22195e 100644 --- a/framework/test/FixtureTrait.php +++ b/framework/test/FixtureTrait.php @@ -24,182 +24,184 @@ */ trait FixtureTrait { - /** - * @var array the list of fixture objects available for the current test. - * The array keys are the corresponding fixture class names. - * The fixtures are listed in their dependency order. That is, fixture A is listed before B - * if B depends on A. - */ - private $_fixtures; - - /** - * Declares the fixtures that are needed by the current test case. - * The return value of this method must be an array of fixture configurations. For example, - * - * ```php - * [ - * // anonymous fixture - * PostFixture::className(), - * // "users" fixture - * 'users' => UserFixture::className(), - * // "cache" fixture with configuration - * 'cache' => [ - * 'class' => CacheFixture::className(), - * 'host' => 'xxx', - * ], - * ] - * ``` - * - * Note that the actual fixtures used for a test case will include both [[globalFixtures()]] - * and [[fixtures()]]. - * - * @return array the fixtures needed by the current test case - */ - public function fixtures() - { - return []; - } - - /** - * Declares the fixtures shared required by different test cases. - * The return value should be similar to that of [[fixtures()]]. - * You should usually override this method in a base class. - * @return array the fixtures shared and required by different test cases. - * @see fixtures() - */ - public function globalFixtures() - { - return []; - } - - /** - * Loads the specified fixtures. - * This method will call [[Fixture::load()]] for every fixture object. - * @param Fixture[] $fixtures the fixtures to be loaded. If this parameter is not specified, - * the return value of [[getFixtures()]] will be used. - */ - public function loadFixtures($fixtures = null) - { - if ($fixtures === null) { - $fixtures = $this->getFixtures(); - } - - /** @var Fixture $fixture */ - foreach ($fixtures as $fixture) { - $fixture->beforeLoad(); - } - foreach ($fixtures as $fixture) { - $fixture->load(); - } - foreach (array_reverse($fixtures) as $fixture) { - $fixture->afterLoad(); - } - } - - /** - * Unloads the specified fixtures. - * This method will call [[Fixture::unload()]] for every fixture object. - * @param Fixture[] $fixtures the fixtures to be loaded. If this parameter is not specified, - * the return value of [[getFixtures()]] will be used. - */ - public function unloadFixtures($fixtures = null) - { - if ($fixtures === null) { - $fixtures = $this->getFixtures(); - } - - /** @var Fixture $fixture */ - foreach ($fixtures as $fixture) { - $fixture->beforeUnload(); - } - $fixtures = array_reverse($fixtures); - foreach ($fixtures as $fixture) { - $fixture->unload(); - } - foreach ($fixtures as $fixture) { - $fixture->afterUnload(); - } - } - - /** - * Returns the fixture objects as specified in [[globalFixtures()]] and [[fixtures()]]. - * @return Fixture[] the loaded fixtures for the current test case - */ - public function getFixtures() - { - if ($this->_fixtures === null) { - $this->_fixtures = $this->createFixtures(array_merge($this->globalFixtures(), $this->fixtures())); - } - return $this->_fixtures; - } - - /** - * Returns the named fixture. - * @param string $name the fixture name. This can be either the fixture alias name, or the class name if the alias is not used. - * @return Fixture the fixture object, or null if the named fixture does not exist. - */ - public function getFixture($name) - { - if ($this->_fixtures === null) { - $this->_fixtures = $this->createFixtures(array_merge($this->globalFixtures(), $this->fixtures())); - } - $name = ltrim($name, '\\'); - return isset($this->_fixtures[$name]) ? $this->_fixtures[$name] : null; - } - - /** - * Creates the specified fixture instances. - * All dependent fixtures will also be created. - * @param array $fixtures the fixtures to be created. You may provide fixture names or fixture configurations. - * If this parameter is not provided, the fixtures specified in [[globalFixtures()]] and [[fixtures()]] will be created. - * @return Fixture[] the created fixture instances - * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among - * the fixtures is detected. - */ - protected function createFixtures(array $fixtures) - { - // normalize fixture configurations - $config = []; // configuration provided in test case - $aliases = []; // class name => alias or class name - foreach ($fixtures as $name => $fixture) { - if (!is_array($fixture)) { - $class = ltrim($fixture, '\\'); - $fixtures[$name] = ['class' => $class]; - $aliases[$class] = is_integer($name) ? $class : $name; - } elseif (isset($fixture['class'])) { - $class = ltrim($fixture['class'], '\\'); - $config[$class] = $fixture; - $aliases[$class] = $name; - } else { - throw new InvalidConfigException("You must specify 'class' for the fixture '$name'."); - } - } - - // create fixture instances - $instances = []; - $stack = array_reverse($fixtures); - while (($fixture = array_pop($stack)) !== null) { - if ($fixture instanceof Fixture) { - $class = get_class($fixture); - $name = isset($aliases[$class]) ? $aliases[$class] : $class; - unset($instances[$name]); // unset so that the fixture is added to the last in the next line - $instances[$name] = $fixture; - } else { - $class = ltrim($fixture['class'], '\\'); - $name = isset($aliases[$class]) ? $aliases[$class] : $class; - if (!isset($instances[$name])) { - $instances[$name] = false; - $stack[] = $fixture = Yii::createObject($fixture); - foreach ($fixture->depends as $dep) { - // need to use the configuration provided in test case - $stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep]; - } - } elseif ($instances[$name] === false) { - throw new InvalidConfigException("A circular dependency is detected for fixture '$class'."); - } - } - } - - return $instances; - } + /** + * @var array the list of fixture objects available for the current test. + * The array keys are the corresponding fixture class names. + * The fixtures are listed in their dependency order. That is, fixture A is listed before B + * if B depends on A. + */ + private $_fixtures; + + /** + * Declares the fixtures that are needed by the current test case. + * The return value of this method must be an array of fixture configurations. For example, + * + * ```php + * [ + * // anonymous fixture + * PostFixture::className(), + * // "users" fixture + * 'users' => UserFixture::className(), + * // "cache" fixture with configuration + * 'cache' => [ + * 'class' => CacheFixture::className(), + * 'host' => 'xxx', + * ], + * ] + * ``` + * + * Note that the actual fixtures used for a test case will include both [[globalFixtures()]] + * and [[fixtures()]]. + * + * @return array the fixtures needed by the current test case + */ + public function fixtures() + { + return []; + } + + /** + * Declares the fixtures shared required by different test cases. + * The return value should be similar to that of [[fixtures()]]. + * You should usually override this method in a base class. + * @return array the fixtures shared and required by different test cases. + * @see fixtures() + */ + public function globalFixtures() + { + return []; + } + + /** + * Loads the specified fixtures. + * This method will call [[Fixture::load()]] for every fixture object. + * @param Fixture[] $fixtures the fixtures to be loaded. If this parameter is not specified, + * the return value of [[getFixtures()]] will be used. + */ + public function loadFixtures($fixtures = null) + { + if ($fixtures === null) { + $fixtures = $this->getFixtures(); + } + + /** @var Fixture $fixture */ + foreach ($fixtures as $fixture) { + $fixture->beforeLoad(); + } + foreach ($fixtures as $fixture) { + $fixture->load(); + } + foreach (array_reverse($fixtures) as $fixture) { + $fixture->afterLoad(); + } + } + + /** + * Unloads the specified fixtures. + * This method will call [[Fixture::unload()]] for every fixture object. + * @param Fixture[] $fixtures the fixtures to be loaded. If this parameter is not specified, + * the return value of [[getFixtures()]] will be used. + */ + public function unloadFixtures($fixtures = null) + { + if ($fixtures === null) { + $fixtures = $this->getFixtures(); + } + + /** @var Fixture $fixture */ + foreach ($fixtures as $fixture) { + $fixture->beforeUnload(); + } + $fixtures = array_reverse($fixtures); + foreach ($fixtures as $fixture) { + $fixture->unload(); + } + foreach ($fixtures as $fixture) { + $fixture->afterUnload(); + } + } + + /** + * Returns the fixture objects as specified in [[globalFixtures()]] and [[fixtures()]]. + * @return Fixture[] the loaded fixtures for the current test case + */ + public function getFixtures() + { + if ($this->_fixtures === null) { + $this->_fixtures = $this->createFixtures(array_merge($this->globalFixtures(), $this->fixtures())); + } + + return $this->_fixtures; + } + + /** + * Returns the named fixture. + * @param string $name the fixture name. This can be either the fixture alias name, or the class name if the alias is not used. + * @return Fixture the fixture object, or null if the named fixture does not exist. + */ + public function getFixture($name) + { + if ($this->_fixtures === null) { + $this->_fixtures = $this->createFixtures(array_merge($this->globalFixtures(), $this->fixtures())); + } + $name = ltrim($name, '\\'); + + return isset($this->_fixtures[$name]) ? $this->_fixtures[$name] : null; + } + + /** + * Creates the specified fixture instances. + * All dependent fixtures will also be created. + * @param array $fixtures the fixtures to be created. You may provide fixture names or fixture configurations. + * If this parameter is not provided, the fixtures specified in [[globalFixtures()]] and [[fixtures()]] will be created. + * @return Fixture[] the created fixture instances + * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among + * the fixtures is detected. + */ + protected function createFixtures(array $fixtures) + { + // normalize fixture configurations + $config = []; // configuration provided in test case + $aliases = []; // class name => alias or class name + foreach ($fixtures as $name => $fixture) { + if (!is_array($fixture)) { + $class = ltrim($fixture, '\\'); + $fixtures[$name] = ['class' => $class]; + $aliases[$class] = is_integer($name) ? $class : $name; + } elseif (isset($fixture['class'])) { + $class = ltrim($fixture['class'], '\\'); + $config[$class] = $fixture; + $aliases[$class] = $name; + } else { + throw new InvalidConfigException("You must specify 'class' for the fixture '$name'."); + } + } + + // create fixture instances + $instances = []; + $stack = array_reverse($fixtures); + while (($fixture = array_pop($stack)) !== null) { + if ($fixture instanceof Fixture) { + $class = get_class($fixture); + $name = isset($aliases[$class]) ? $aliases[$class] : $class; + unset($instances[$name]); // unset so that the fixture is added to the last in the next line + $instances[$name] = $fixture; + } else { + $class = ltrim($fixture['class'], '\\'); + $name = isset($aliases[$class]) ? $aliases[$class] : $class; + if (!isset($instances[$name])) { + $instances[$name] = false; + $stack[] = $fixture = Yii::createObject($fixture); + foreach ($fixture->depends as $dep) { + // need to use the configuration provided in test case + $stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep]; + } + } elseif ($instances[$name] === false) { + throw new InvalidConfigException("A circular dependency is detected for fixture '$class'."); + } + } + } + + return $instances; + } } diff --git a/framework/test/InitDbFixture.php b/framework/test/InitDbFixture.php index b54e52fd098..b399979b8ca 100644 --- a/framework/test/InitDbFixture.php +++ b/framework/test/InitDbFixture.php @@ -26,72 +26,71 @@ */ class InitDbFixture extends DbFixture { - /** - * @var string the init script file that should be executed when loading this fixture. - * This should be either a file path or path alias. Note that if the file does not exist, - * no error will be raised. - */ - public $initScript = '@app/tests/fixtures/initdb.php'; - /** - * @var array list of database schemas that the test tables may reside in. Defaults to - * `['']`, meaning using the default schema (an empty string refers to the - * default schema). This property is mainly used when turning on and off integrity checks - * so that fixture data can be populated into the database without causing problem. - */ - public $schemas = ['']; + /** + * @var string the init script file that should be executed when loading this fixture. + * This should be either a file path or path alias. Note that if the file does not exist, + * no error will be raised. + */ + public $initScript = '@app/tests/fixtures/initdb.php'; + /** + * @var array list of database schemas that the test tables may reside in. Defaults to + * `['']`, meaning using the default schema (an empty string refers to the + * default schema). This property is mainly used when turning on and off integrity checks + * so that fixture data can be populated into the database without causing problem. + */ + public $schemas = ['']; + /** + * @inheritdoc + */ + public function beforeLoad() + { + $this->checkIntegrity(false); + } - /** - * @inheritdoc - */ - public function beforeLoad() - { - $this->checkIntegrity(false); - } + /** + * @inheritdoc + */ + public function afterLoad() + { + $this->checkIntegrity(true); + } - /** - * @inheritdoc - */ - public function afterLoad() - { - $this->checkIntegrity(true); - } + /** + * @inheritdoc + */ + public function load() + { + $file = Yii::getAlias($this->initScript); + if (is_file($file)) { + require($file); + } + } - /** - * @inheritdoc - */ - public function load() - { - $file = Yii::getAlias($this->initScript); - if (is_file($file)) { - require($file); - } - } + /** + * @inheritdoc + */ + public function beforeUnload() + { + $this->checkIntegrity(false); + } - /** - * @inheritdoc - */ - public function beforeUnload() - { - $this->checkIntegrity(false); - } + /** + * @inheritdoc + */ + public function afterUnload() + { + $this->checkIntegrity(true); + } - /** - * @inheritdoc - */ - public function afterUnload() - { - $this->checkIntegrity(true); - } - - /** - * Toggles the DB integrity check. - * @param boolean $check whether to turn on or off the integrity check. - */ - public function checkIntegrity($check) - { - foreach ($this->schemas as $schema) { - $this->db->createCommand()->checkIntegrity($check, $schema)->execute(); - } - } + /** + * Toggles the DB integrity check. + * @param boolean $check whether to turn on or off the integrity check. + */ + public function checkIntegrity($check) + { + foreach ($this->schemas as $schema) { + $this->db->createCommand()->checkIntegrity($check, $schema)->execute(); + } + } } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 8f66dc3a9a6..d8859098535 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -20,71 +20,72 @@ */ class BooleanValidator extends Validator { - /** - * @var mixed the value representing true status. Defaults to '1'. - */ - public $trueValue = '1'; - /** - * @var mixed the value representing false status. Defaults to '0'. - */ - public $falseValue = '0'; - /** - * @var boolean whether the comparison to [[trueValue]] and [[falseValue]] is strict. - * When this is true, the attribute value and type must both match those of [[trueValue]] or [[falseValue]]. - * Defaults to false, meaning only the value needs to be matched. - */ - public $strict = false; + /** + * @var mixed the value representing true status. Defaults to '1'. + */ + public $trueValue = '1'; + /** + * @var mixed the value representing false status. Defaults to '0'. + */ + public $falseValue = '0'; + /** + * @var boolean whether the comparison to [[trueValue]] and [[falseValue]] is strict. + * When this is true, the attribute value and type must both match those of [[trueValue]] or [[falseValue]]. + * Defaults to false, meaning only the value needs to be matched. + */ + public $strict = false; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} must be either "{true}" or "{false}".'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} must be either "{true}" or "{false}".'); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - $valid = !$this->strict && ($value == $this->trueValue || $value == $this->falseValue) - || $this->strict && ($value === $this->trueValue || $value === $this->falseValue); - if (!$valid) { - return [$this->message, [ - 'true' => $this->trueValue, - 'false' => $this->falseValue, - ]]; - } else { - return null; - } - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + $valid = !$this->strict && ($value == $this->trueValue || $value == $this->falseValue) + || $this->strict && ($value === $this->trueValue || $value === $this->falseValue); + if (!$valid) { + return [$this->message, [ + 'true' => $this->trueValue, + 'false' => $this->falseValue, + ]]; + } else { + return null; + } + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $options = [ - 'trueValue' => $this->trueValue, - 'falseValue' => $this->falseValue, - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - 'true' => $this->trueValue, - 'false' => $this->falseValue, - ], Yii::$app->language), - ]; - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } - if ($this->strict) { - $options['strict'] = 1; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $options = [ + 'trueValue' => $this->trueValue, + 'falseValue' => $this->falseValue, + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + 'true' => $this->trueValue, + 'false' => $this->falseValue, + ], Yii::$app->language), + ]; + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + if ($this->strict) { + $options['strict'] = 1; + } - ValidationAsset::register($view); - return 'yii.validation.boolean(value, messages, ' . json_encode($options) . ');'; - } + ValidationAsset::register($view); + + return 'yii.validation.boolean(value, messages, ' . json_encode($options) . ');'; + } } diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index d1e6337466e..128da1e32c4 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -29,177 +29,178 @@ */ class CompareValidator extends Validator { - /** - * @var string the name of the attribute to be compared with. When both this property - * and [[compareValue]] are set, the latter takes precedence. If neither is set, - * it assumes the comparison is against another attribute whose name is formed by - * appending '_repeat' to the attribute being validated. For example, if 'password' is - * being validated, then the attribute to be compared would be 'password_repeat'. - * @see compareValue - */ - public $compareAttribute; - /** - * @var mixed the constant value to be compared with. When both this property - * and [[compareAttribute]] are set, this property takes precedence. - * @see compareAttribute - */ - public $compareValue; - /** - * @var string the operator for comparison. The following operators are supported: - * - * - '==': validates to see if the two values are equal. The comparison is done is non-strict mode. - * - '===': validates to see if the two values are equal. The comparison is done is strict mode. - * - '!=': validates to see if the two values are NOT equal. The comparison is done is non-strict mode. - * - '!==': validates to see if the two values are NOT equal. The comparison is done is strict mode. - * - `>`: validates to see if the value being validated is greater than the value being compared with. - * - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with. - * - `<`: validates to see if the value being validated is less than the value being compared with. - * - `<=`: validates to see if the value being validated is less than or equal to the value being compared with. - */ - public $operator = '=='; - /** - * @var string the user-defined error message. It may contain the following placeholders which - * will be replaced accordingly by the validator: - * - * - `{attribute}`: the label of the attribute being validated - * - `{value}`: the value of the attribute being validated - * - `{compareValue}`: the value or the attribute label to be compared with - * - `{compareAttribute}`: the label of the attribute to be compared with - */ - public $message; + /** + * @var string the name of the attribute to be compared with. When both this property + * and [[compareValue]] are set, the latter takes precedence. If neither is set, + * it assumes the comparison is against another attribute whose name is formed by + * appending '_repeat' to the attribute being validated. For example, if 'password' is + * being validated, then the attribute to be compared would be 'password_repeat'. + * @see compareValue + */ + public $compareAttribute; + /** + * @var mixed the constant value to be compared with. When both this property + * and [[compareAttribute]] are set, this property takes precedence. + * @see compareAttribute + */ + public $compareValue; + /** + * @var string the operator for comparison. The following operators are supported: + * + * - '==': validates to see if the two values are equal. The comparison is done is non-strict mode. + * - '===': validates to see if the two values are equal. The comparison is done is strict mode. + * - '!=': validates to see if the two values are NOT equal. The comparison is done is non-strict mode. + * - '!==': validates to see if the two values are NOT equal. The comparison is done is strict mode. + * - `>`: validates to see if the value being validated is greater than the value being compared with. + * - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with. + * - `<`: validates to see if the value being validated is less than the value being compared with. + * - `<=`: validates to see if the value being validated is less than or equal to the value being compared with. + */ + public $operator = '=='; + /** + * @var string the user-defined error message. It may contain the following placeholders which + * will be replaced accordingly by the validator: + * + * - `{attribute}`: the label of the attribute being validated + * - `{value}`: the value of the attribute being validated + * - `{compareValue}`: the value or the attribute label to be compared with + * - `{compareAttribute}`: the label of the attribute to be compared with + */ + public $message; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + switch ($this->operator) { + case '==': + $this->message = Yii::t('yii', '{attribute} must be repeated exactly.'); + break; + case '===': + $this->message = Yii::t('yii', '{attribute} must be repeated exactly.'); + break; + case '!=': + $this->message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + break; + case '!==': + $this->message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + break; + case '>': + $this->message = Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + break; + case '>=': + $this->message = Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + break; + case '<': + $this->message = Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + break; + case '<=': + $this->message = Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + break; + default: + throw new InvalidConfigException("Unknown operator: {$this->operator}"); + } + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - switch ($this->operator) { - case '==': - $this->message = Yii::t('yii', '{attribute} must be repeated exactly.'); - break; - case '===': - $this->message = Yii::t('yii', '{attribute} must be repeated exactly.'); - break; - case '!=': - $this->message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); - break; - case '!==': - $this->message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); - break; - case '>': - $this->message = Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); - break; - case '>=': - $this->message = Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); - break; - case '<': - $this->message = Yii::t('yii', '{attribute} must be less than "{compareValue}".'); - break; - case '<=': - $this->message = Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); - } - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $value = $object->$attribute; + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $value = $object->$attribute; - if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - return; - } - if ($this->compareValue !== null) { - $compareLabel = $compareValue = $this->compareValue; - } else { - $compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute; - $compareValue = $object->$compareAttribute; - $compareLabel = $object->getAttributeLabel($compareAttribute); - } + return; + } + if ($this->compareValue !== null) { + $compareLabel = $compareValue = $this->compareValue; + } else { + $compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute; + $compareValue = $object->$compareAttribute; + $compareLabel = $object->getAttributeLabel($compareAttribute); + } - if (!$this->compareValues($this->operator, $value, $compareValue)) { - $this->addError($object, $attribute, $this->message, [ - 'compareAttribute' => $compareLabel, - 'compareValue' => $compareValue, - ]); - } - } + if (!$this->compareValues($this->operator, $value, $compareValue)) { + $this->addError($object, $attribute, $this->message, [ + 'compareAttribute' => $compareLabel, + 'compareValue' => $compareValue, + ]); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - if ($this->compareValue === null) { - throw new InvalidConfigException('CompareValidator::compareValue must be set.'); - } - if (!$this->compareValues($this->operator, $value, $this->compareValue)) { - return [$this->message, [ - 'compareAttribute' => $this->compareValue, - 'compareValue' => $this->compareValue, - ]]; - } else { - return null; - } - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if ($this->compareValue === null) { + throw new InvalidConfigException('CompareValidator::compareValue must be set.'); + } + if (!$this->compareValues($this->operator, $value, $this->compareValue)) { + return [$this->message, [ + 'compareAttribute' => $this->compareValue, + 'compareValue' => $this->compareValue, + ]]; + } else { + return null; + } + } - /** - * Compares two values with the specified operator. - * @param string $operator the comparison operator - * @param mixed $value the value being compared - * @param mixed $compareValue another value being compared - * @return boolean whether the comparison using the specified operator is true. - */ - protected function compareValues($operator, $value, $compareValue) - { - switch ($operator) { - case '==': return $value == $compareValue; - case '===': return $value === $compareValue; - case '!=': return $value != $compareValue; - case '!==': return $value !== $compareValue; - case '>': return $value > $compareValue; - case '>=': return $value >= $compareValue; - case '<': return $value < $compareValue; - case '<=': return $value <= $compareValue; - default: return false; - } - } + /** + * Compares two values with the specified operator. + * @param string $operator the comparison operator + * @param mixed $value the value being compared + * @param mixed $compareValue another value being compared + * @return boolean whether the comparison using the specified operator is true. + */ + protected function compareValues($operator, $value, $compareValue) + { + switch ($operator) { + case '==': return $value == $compareValue; + case '===': return $value === $compareValue; + case '!=': return $value != $compareValue; + case '!==': return $value !== $compareValue; + case '>': return $value > $compareValue; + case '>=': return $value >= $compareValue; + case '<': return $value < $compareValue; + case '<=': return $value <= $compareValue; + default: return false; + } + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $options = ['operator' => $this->operator]; + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $options = ['operator' => $this->operator]; - if ($this->compareValue !== null) { - $options['compareValue'] = $this->compareValue; - $compareValue = $this->compareValue; - } else { - $compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute; - $compareValue = $object->getAttributeLabel($compareAttribute); - $options['compareAttribute'] = Html::getInputId($object, $compareAttribute); - } + if ($this->compareValue !== null) { + $options['compareValue'] = $this->compareValue; + $compareValue = $this->compareValue; + } else { + $compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute; + $compareValue = $object->getAttributeLabel($compareAttribute); + $options['compareAttribute'] = Html::getInputId($object, $compareAttribute); + } - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } - $options['message'] = Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - 'compareAttribute' => $compareValue, - 'compareValue' => $compareValue, - ], Yii::$app->language); + $options['message'] = Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + 'compareAttribute' => $compareValue, + 'compareValue' => $compareValue, + ], Yii::$app->language); - ValidationAsset::register($view); - return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; - } + ValidationAsset::register($view); + + return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; + } } diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index d79aa857c81..7819f802378 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -18,56 +18,57 @@ */ class DateValidator extends Validator { - /** - * @var string the date format that the value being validated should follow. - * Please refer to on - * supported formats. - */ - public $format = 'Y-m-d'; - /** - * @var string the name of the attribute to receive the parsing result. - * When this property is not null and the validation is successful, the named attribute will - * receive the parsing result. - */ - public $timestampAttribute; + /** + * @var string the date format that the value being validated should follow. + * Please refer to on + * supported formats. + */ + public $format = 'Y-m-d'; + /** + * @var string the name of the attribute to receive the parsing result. + * When this property is not null and the validation is successful, the named attribute will + * receive the parsing result. + */ + public $timestampAttribute; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = Yii::t('yii', 'The format of {attribute} is invalid.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii', 'The format of {attribute} is invalid.'); + } + } - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $value = $object->$attribute; - $result = $this->validateValue($value); - if (!empty($result)) { - $this->addError($object, $attribute, $result[0], $result[1]); - } elseif ($this->timestampAttribute !== null) { - $date = DateTime::createFromFormat($this->format, $value); - $object->{$this->timestampAttribute} = $date->getTimestamp(); - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $value = $object->$attribute; + $result = $this->validateValue($value); + if (!empty($result)) { + $this->addError($object, $attribute, $result[0], $result[1]); + } elseif ($this->timestampAttribute !== null) { + $date = DateTime::createFromFormat($this->format, $value); + $object->{$this->timestampAttribute} = $date->getTimestamp(); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - if (is_array($value)) { - return [$this->message, []]; - } - $date = DateTime::createFromFormat($this->format, $value); - $errors = DateTime::getLastErrors(); - $invalid = $date === false || $errors['error_count'] || $errors['warning_count']; - return $invalid ? [$this->message, []] : null; - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if (is_array($value)) { + return [$this->message, []]; + } + $date = DateTime::createFromFormat($this->format, $value); + $errors = DateTime::getLastErrors(); + $invalid = $date === false || $errors['error_count'] || $errors['warning_count']; + + return $invalid ? [$this->message, []] : null; + } } diff --git a/framework/validators/DefaultValueValidator.php b/framework/validators/DefaultValueValidator.php index 0db3a670034..e4ad59b7e57 100644 --- a/framework/validators/DefaultValueValidator.php +++ b/framework/validators/DefaultValueValidator.php @@ -18,23 +18,23 @@ */ class DefaultValueValidator extends Validator { - /** - * @var mixed the default value to be set to the specified attributes. - */ - public $value; - /** - * @var boolean this property is overwritten to be false so that this validator will - * be applied when the value being validated is empty. - */ - public $skipOnEmpty = false; + /** + * @var mixed the default value to be set to the specified attributes. + */ + public $value; + /** + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. + */ + public $skipOnEmpty = false; - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - if ($this->isEmpty($object->$attribute)) { - $object->$attribute = $this->value; - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + if ($this->isEmpty($object->$attribute)) { + $object->$attribute = $this->value; + } + } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index aa63a57eb7b..2056fd8eea3 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -20,96 +20,97 @@ */ class EmailValidator extends Validator { - /** - * @var string the regular expression used to validate the attribute value. - * @see http://www.regular-expressions.info/email.html - */ - public $pattern = '/^[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/'; - /** - * @var string the regular expression used to validate email addresses with the name part. - * This property is used only when [[allowName]] is true. - * @see allowName - */ - public $fullPattern = '/^[^@]*<[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?>$/'; - /** - * @var boolean whether to allow name in the email address (e.g. "John Smith "). Defaults to false. - * @see fullPattern - */ - public $allowName = false; - /** - * @var boolean whether to check whether the emails domain exists and has either an A or MX record. - * Be aware of the fact that this check can fail due to temporary DNS problems even if the email address is - * valid and an email would be deliverable. Defaults to false. - */ - public $checkDNS = false; - /** - * @var boolean whether validation process should take into account IDN (internationalized domain - * names). Defaults to false meaning that validation of emails containing IDN will always fail. - * Note that in order to use IDN validation you have to install and enable `intl` PHP extension, - * otherwise an exception would be thrown. - */ - public $enableIDN = false; + /** + * @var string the regular expression used to validate the attribute value. + * @see http://www.regular-expressions.info/email.html + */ + public $pattern = '/^[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/'; + /** + * @var string the regular expression used to validate email addresses with the name part. + * This property is used only when [[allowName]] is true. + * @see allowName + */ + public $fullPattern = '/^[^@]*<[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?>$/'; + /** + * @var boolean whether to allow name in the email address (e.g. "John Smith "). Defaults to false. + * @see fullPattern + */ + public $allowName = false; + /** + * @var boolean whether to check whether the emails domain exists and has either an A or MX record. + * Be aware of the fact that this check can fail due to temporary DNS problems even if the email address is + * valid and an email would be deliverable. Defaults to false. + */ + public $checkDNS = false; + /** + * @var boolean whether validation process should take into account IDN (internationalized domain + * names). Defaults to false meaning that validation of emails containing IDN will always fail. + * Note that in order to use IDN validation you have to install and enable `intl` PHP extension, + * otherwise an exception would be thrown. + */ + public $enableIDN = false; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->enableIDN && !function_exists('idn_to_ascii')) { + throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} is not a valid email address.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->enableIDN && !function_exists('idn_to_ascii')) { - throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.'); - } - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} is not a valid email address.'); - } - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + // make sure string length is limited to avoid DOS attacks + if (!is_string($value) || strlen($value) >= 320) { + $valid = false; + } elseif (!preg_match('/^(.*?)$/', $value, $matches)) { + $valid = false; + } else { + $domain = $matches[3]; + if ($this->enableIDN) { + $value = $matches[1] . idn_to_ascii($matches[2]) . '@' . idn_to_ascii($domain) . $matches[4]; + } + $valid = preg_match($this->pattern, $value) || $this->allowName && preg_match($this->fullPattern, $value); + if ($valid && $this->checkDNS) { + $valid = checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A'); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - // make sure string length is limited to avoid DOS attacks - if (!is_string($value) || strlen($value) >= 320) { - $valid = false; - } elseif (!preg_match('/^(.*?)$/', $value, $matches)) { - $valid = false; - } else { - $domain = $matches[3]; - if ($this->enableIDN) { - $value = $matches[1] . idn_to_ascii($matches[2]) . '@' . idn_to_ascii($domain) . $matches[4]; - } - $valid = preg_match($this->pattern, $value) || $this->allowName && preg_match($this->fullPattern, $value); - if ($valid && $this->checkDNS) { - $valid = checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A'); - } - } - return $valid ? null : [$this->message, []]; - } + return $valid ? null : [$this->message, []]; + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $options = [ - 'pattern' => new JsExpression($this->pattern), - 'fullPattern' => new JsExpression($this->fullPattern), - 'allowName' => $this->allowName, - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - ], Yii::$app->language), - 'enableIDN' => (boolean)$this->enableIDN, - ]; - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $options = [ + 'pattern' => new JsExpression($this->pattern), + 'fullPattern' => new JsExpression($this->fullPattern), + 'allowName' => $this->allowName, + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + ], Yii::$app->language), + 'enableIDN' => (boolean) $this->enableIDN, + ]; + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } - ValidationAsset::register($view); - if ($this->enableIDN) { - PunycodeAsset::register($view); - } - return 'yii.validation.email(value, messages, ' . Json::encode($options) . ');'; - } + ValidationAsset::register($view); + if ($this->enableIDN) { + PunycodeAsset::register($view); + } + + return 'yii.validation.email(value, messages, ' . Json::encode($options) . ');'; + } } diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index 076066ba2f6..a714fc888ce 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -39,107 +39,108 @@ */ class ExistValidator extends Validator { - /** - * @var string the name of the ActiveRecord class that should be used to validate the existence - * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated. - * @see targetAttribute - */ - public $targetClass; - /** - * @var string|array the name of the ActiveRecord attribute that should be used to - * validate the existence of the current attribute value. If not set, it will use the name - * of the attribute currently being validated. You may use an array to validate the existence - * of multiple columns at the same time. The array values are the attributes that will be - * used to validate the existence, while the array keys are the attributes whose values are to be validated. - * If the key and the value are the same, you can just specify the value. - */ - public $targetAttribute; - /** - * @var string|array|\Closure additional filter to be applied to the DB query used to check the existence of the attribute value. - * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]] - * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` - * is the [[\yii\db\Query|Query]] object that you can modify in the function. - */ - public $filter; + /** + * @var string the name of the ActiveRecord class that should be used to validate the existence + * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated. + * @see targetAttribute + */ + public $targetClass; + /** + * @var string|array the name of the ActiveRecord attribute that should be used to + * validate the existence of the current attribute value. If not set, it will use the name + * of the attribute currently being validated. You may use an array to validate the existence + * of multiple columns at the same time. The array values are the attributes that will be + * used to validate the existence, while the array keys are the attributes whose values are to be validated. + * If the key and the value are the same, you can just specify the value. + */ + public $targetAttribute; + /** + * @var string|array|\Closure additional filter to be applied to the DB query used to check the existence of the attribute value. + * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]] + * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` + * is the [[\yii\db\Query|Query]] object that you can modify in the function. + */ + public $filter; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} is invalid.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} is invalid.'); - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; + if (is_array($targetAttribute)) { + $params = []; + foreach ($targetAttribute as $k => $v) { + $params[$v] = is_integer($k) ? $object->$v : $object->$k; + } + } else { + $params = [$targetAttribute => $object->$attribute]; + } - if (is_array($targetAttribute)) { - $params = []; - foreach ($targetAttribute as $k => $v) { - $params[$v] = is_integer($k) ? $object->$v : $object->$k; - } - } else { - $params = [$targetAttribute => $object->$attribute]; - } + foreach ($params as $value) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - foreach ($params as $value) { - if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - return; - } - } + return; + } + } - $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass; - $query = $this->createQuery($targetClass, $params); + $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass; + $query = $this->createQuery($targetClass, $params); - if (!$query->exists()) { - $this->addError($object, $attribute, $this->message); - } - } + if (!$query->exists()) { + $this->addError($object, $attribute, $this->message); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - if (is_array($value)) { - return [$this->message, []]; - } - if ($this->targetClass === null) { - throw new InvalidConfigException('The "targetClass" property must be set.'); - } - if (!is_string($this->targetAttribute)) { - throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.'); - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if (is_array($value)) { + return [$this->message, []]; + } + if ($this->targetClass === null) { + throw new InvalidConfigException('The "targetClass" property must be set.'); + } + if (!is_string($this->targetAttribute)) { + throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.'); + } - $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]); + $query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]); - return $query->exists() ? null : [$this->message, []]; - } + return $query->exists() ? null : [$this->message, []]; + } - /** - * Creates a query instance with the given condition. - * @param string $targetClass the target AR class - * @param mixed $condition query condition - * @return \yii\db\ActiveQueryInterface the query instance - */ - protected function createQuery($targetClass, $condition) - { - /** @var \yii\db\ActiveRecordInterface $targetClass */ - $query = $targetClass::find()->where($condition); - if ($this->filter instanceof \Closure) { - call_user_func($this->filter, $query); - } elseif ($this->filter !== null) { - $query->andWhere($this->filter); - } - return $query; - } + /** + * Creates a query instance with the given condition. + * @param string $targetClass the target AR class + * @param mixed $condition query condition + * @return \yii\db\ActiveQueryInterface the query instance + */ + protected function createQuery($targetClass, $condition) + { + /** @var \yii\db\ActiveRecordInterface $targetClass */ + $query = $targetClass::find()->where($condition); + if ($this->filter instanceof \Closure) { + call_user_func($this->filter, $query); + } elseif ($this->filter !== null) { + $query->andWhere($this->filter); + } + + return $query; + } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index bf565389166..86f0dd203e5 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -20,240 +20,243 @@ */ class FileValidator extends Validator { - /** - * @var array|string a list of file name extensions that are allowed to be uploaded. - * This can be either an array or a string consisting of file extension names - * separated by space or comma (e.g. "gif, jpg"). - * Extension names are case-insensitive. Defaults to null, meaning all file name - * extensions are allowed. - * @see wrongType - */ - public $types; - /** - * @var integer the minimum number of bytes required for the uploaded file. - * Defaults to null, meaning no limit. - * @see tooSmall - */ - public $minSize; - /** - * @var integer the maximum number of bytes required for the uploaded file. - * Defaults to null, meaning no limit. - * Note, the size limit is also affected by 'upload_max_filesize' INI setting - * and the 'MAX_FILE_SIZE' hidden field value. - * @see tooBig - */ - public $maxSize; - /** - * @var integer the maximum file count the given attribute can hold. - * It defaults to 1, meaning single file upload. By defining a higher number, - * multiple uploads become possible. - * @see tooMany - */ - public $maxFiles = 1; - /** - * @var string the error message used when a file is not uploaded correctly. - */ - public $message; - /** - * @var string the error message used when no file is uploaded. - */ - public $uploadRequired; - /** - * @var string the error message used when the uploaded file is too large. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {limit}: the maximum size allowed (see [[getSizeLimit()]]) - */ - public $tooBig; - /** - * @var string the error message used when the uploaded file is too small. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {limit}: the value of [[minSize]] - */ - public $tooSmall; - /** - * @var string the error message used when the uploaded file has an extension name - * that is not listed in [[types]]. You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {extensions}: the list of the allowed extensions. - */ - public $wrongType; - /** - * @var string the error message used if the count of multiple uploads exceeds limit. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {limit}: the value of [[maxFiles]] - */ - public $tooMany; + /** + * @var array|string a list of file name extensions that are allowed to be uploaded. + * This can be either an array or a string consisting of file extension names + * separated by space or comma (e.g. "gif, jpg"). + * Extension names are case-insensitive. Defaults to null, meaning all file name + * extensions are allowed. + * @see wrongType + */ + public $types; + /** + * @var integer the minimum number of bytes required for the uploaded file. + * Defaults to null, meaning no limit. + * @see tooSmall + */ + public $minSize; + /** + * @var integer the maximum number of bytes required for the uploaded file. + * Defaults to null, meaning no limit. + * Note, the size limit is also affected by 'upload_max_filesize' INI setting + * and the 'MAX_FILE_SIZE' hidden field value. + * @see tooBig + */ + public $maxSize; + /** + * @var integer the maximum file count the given attribute can hold. + * It defaults to 1, meaning single file upload. By defining a higher number, + * multiple uploads become possible. + * @see tooMany + */ + public $maxFiles = 1; + /** + * @var string the error message used when a file is not uploaded correctly. + */ + public $message; + /** + * @var string the error message used when no file is uploaded. + */ + public $uploadRequired; + /** + * @var string the error message used when the uploaded file is too large. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the maximum size allowed (see [[getSizeLimit()]]) + */ + public $tooBig; + /** + * @var string the error message used when the uploaded file is too small. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[minSize]] + */ + public $tooSmall; + /** + * @var string the error message used when the uploaded file has an extension name + * that is not listed in [[types]]. You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {extensions}: the list of the allowed extensions. + */ + public $wrongType; + /** + * @var string the error message used if the count of multiple uploads exceeds limit. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {limit}: the value of [[maxFiles]] + */ + public $tooMany; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = Yii::t('yii', 'File upload failed.'); - } - if ($this->uploadRequired === null) { - $this->uploadRequired = Yii::t('yii', 'Please upload a file.'); - } - if ($this->tooMany === null) { - $this->tooMany = Yii::t('yii', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.'); - } - if ($this->wrongType === null) { - $this->wrongType = Yii::t('yii', 'Only files with these extensions are allowed: {extensions}.'); - } - if ($this->tooBig === null) { - $this->tooBig = Yii::t('yii', 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.'); - } - if ($this->tooSmall === null) { - $this->tooSmall = Yii::t('yii', 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.'); - } - if (!is_array($this->types)) { - $this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii', 'File upload failed.'); + } + if ($this->uploadRequired === null) { + $this->uploadRequired = Yii::t('yii', 'Please upload a file.'); + } + if ($this->tooMany === null) { + $this->tooMany = Yii::t('yii', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.'); + } + if ($this->wrongType === null) { + $this->wrongType = Yii::t('yii', 'Only files with these extensions are allowed: {extensions}.'); + } + if ($this->tooBig === null) { + $this->tooBig = Yii::t('yii', 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.'); + } + if ($this->tooSmall === null) { + $this->tooSmall = Yii::t('yii', 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.'); + } + if (!is_array($this->types)) { + $this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); + } + } - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - if ($this->maxFiles > 1) { - $files = $object->$attribute; - if (!is_array($files)) { - $this->addError($object, $attribute, $this->uploadRequired); - return; - } - foreach ($files as $i => $file) { - if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) { - unset($files[$i]); - } - } - $object->$attribute = array_values($files); - if (empty($files)) { - $this->addError($object, $attribute, $this->uploadRequired); - } - if (count($files) > $this->maxFiles) { - $this->addError($object, $attribute, $this->tooMany, ['limit' => $this->maxFiles]); - } else { - foreach ($files as $file) { - $result = $this->validateValue($file); - if (!empty($result)) { - $this->addError($object, $attribute, $result[0], $result[1]); - } - } - } - } else { - $result = $this->validateValue($object->$attribute); - if (!empty($result)) { - $this->addError($object, $attribute, $result[0], $result[1]); - } - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + if ($this->maxFiles > 1) { + $files = $object->$attribute; + if (!is_array($files)) { + $this->addError($object, $attribute, $this->uploadRequired); - /** - * @inheritdoc - */ - protected function validateValue($file) - { - if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) { - return [$this->uploadRequired, []]; - } - switch ($file->error) { - case UPLOAD_ERR_OK: - if ($this->maxSize !== null && $file->size > $this->maxSize) { - return [$this->tooBig, ['file' => $file->name, 'limit' => $this->getSizeLimit()]]; - } elseif ($this->minSize !== null && $file->size < $this->minSize) { - return [$this->tooSmall, ['file' => $file->name, 'limit' => $this->minSize]]; - } elseif (!empty($this->types) && !in_array(strtolower(pathinfo($file->name, PATHINFO_EXTENSION)), $this->types, true)) { - return [$this->wrongType, ['file' => $file->name, 'extensions' => implode(', ', $this->types)]]; - } else { - return null; - } - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - return [$this->tooBig, ['file' => $file->name, 'limit' => $this->getSizeLimit()]]; - case UPLOAD_ERR_PARTIAL: - Yii::warning('File was only partially uploaded: ' . $file->name, __METHOD__); - break; - case UPLOAD_ERR_NO_TMP_DIR: - Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->name, __METHOD__); - break; - case UPLOAD_ERR_CANT_WRITE: - Yii::warning('Failed to write the uploaded file to disk: ' . $file->name, __METHOD__); - break; - case UPLOAD_ERR_EXTENSION: - Yii::warning('File upload was stopped by some PHP extension: ' . $file->name, __METHOD__); - break; - default: - break; - } - return [$this->message, []]; - } + return; + } + foreach ($files as $i => $file) { + if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) { + unset($files[$i]); + } + } + $object->$attribute = array_values($files); + if (empty($files)) { + $this->addError($object, $attribute, $this->uploadRequired); + } + if (count($files) > $this->maxFiles) { + $this->addError($object, $attribute, $this->tooMany, ['limit' => $this->maxFiles]); + } else { + foreach ($files as $file) { + $result = $this->validateValue($file); + if (!empty($result)) { + $this->addError($object, $attribute, $result[0], $result[1]); + } + } + } + } else { + $result = $this->validateValue($object->$attribute); + if (!empty($result)) { + $this->addError($object, $attribute, $result[0], $result[1]); + } + } + } - /** - * Returns the maximum size allowed for uploaded files. - * This is determined based on three factors: - * - * - 'upload_max_filesize' in php.ini - * - 'MAX_FILE_SIZE' hidden field - * - [[maxSize]] - * - * @return integer the size limit for uploaded files. - */ - public function getSizeLimit() - { - $limit = ini_get('upload_max_filesize'); - $limit = $this->sizeToBytes($limit); - if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) { - $limit = $this->maxSize; - } - if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) { - $limit = (int)$_POST['MAX_FILE_SIZE']; - } - return $limit; - } + /** + * @inheritdoc + */ + protected function validateValue($file) + { + if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) { + return [$this->uploadRequired, []]; + } + switch ($file->error) { + case UPLOAD_ERR_OK: + if ($this->maxSize !== null && $file->size > $this->maxSize) { + return [$this->tooBig, ['file' => $file->name, 'limit' => $this->getSizeLimit()]]; + } elseif ($this->minSize !== null && $file->size < $this->minSize) { + return [$this->tooSmall, ['file' => $file->name, 'limit' => $this->minSize]]; + } elseif (!empty($this->types) && !in_array(strtolower(pathinfo($file->name, PATHINFO_EXTENSION)), $this->types, true)) { + return [$this->wrongType, ['file' => $file->name, 'extensions' => implode(', ', $this->types)]]; + } else { + return null; + } + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + return [$this->tooBig, ['file' => $file->name, 'limit' => $this->getSizeLimit()]]; + case UPLOAD_ERR_PARTIAL: + Yii::warning('File was only partially uploaded: ' . $file->name, __METHOD__); + break; + case UPLOAD_ERR_NO_TMP_DIR: + Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->name, __METHOD__); + break; + case UPLOAD_ERR_CANT_WRITE: + Yii::warning('Failed to write the uploaded file to disk: ' . $file->name, __METHOD__); + break; + case UPLOAD_ERR_EXTENSION: + Yii::warning('File upload was stopped by some PHP extension: ' . $file->name, __METHOD__); + break; + default: + break; + } - /** - * @inheritdoc - */ - public function isEmpty($value, $trim = false) - { - $value = is_array($value) && !empty($value) ? $value[0] : $value; + return [$this->message, []]; + } - return !$value instanceof UploadedFile || $value->error == UPLOAD_ERR_NO_FILE; - } + /** + * Returns the maximum size allowed for uploaded files. + * This is determined based on three factors: + * + * - 'upload_max_filesize' in php.ini + * - 'MAX_FILE_SIZE' hidden field + * - [[maxSize]] + * + * @return integer the size limit for uploaded files. + */ + public function getSizeLimit() + { + $limit = ini_get('upload_max_filesize'); + $limit = $this->sizeToBytes($limit); + if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) { + $limit = $this->maxSize; + } + if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) { + $limit = (int) $_POST['MAX_FILE_SIZE']; + } - /** - * Converts php.ini style size to bytes - * - * @param string $sizeStr $sizeStr - * @return int - */ - private function sizeToBytes($sizeStr) - { - switch (substr($sizeStr, -1)) { - case 'M': - case 'm': - return (int)$sizeStr * 1048576; - case 'K': - case 'k': - return (int)$sizeStr * 1024; - case 'G': - case 'g': - return (int)$sizeStr * 1073741824; - default: - return (int)$sizeStr; - } - } + return $limit; + } + + /** + * @inheritdoc + */ + public function isEmpty($value, $trim = false) + { + $value = is_array($value) && !empty($value) ? $value[0] : $value; + + return !$value instanceof UploadedFile || $value->error == UPLOAD_ERR_NO_FILE; + } + + /** + * Converts php.ini style size to bytes + * + * @param string $sizeStr $sizeStr + * @return int + */ + private function sizeToBytes($sizeStr) + { + switch (substr($sizeStr, -1)) { + case 'M': + case 'm': + return (int) $sizeStr * 1048576; + case 'K': + case 'k': + return (int) $sizeStr * 1024; + case 'G': + case 'g': + return (int) $sizeStr * 1073741824; + default: + return (int) $sizeStr; + } + } } diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index 06a52d9eee8..b196b4ddaf3 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -30,37 +30,37 @@ */ class FilterValidator extends Validator { - /** - * @var callable the filter. This can be a global function name, anonymous function, etc. - * The function signature must be as follows, - * - * ~~~ - * function foo($value) {...return $newValue; } - * ~~~ - */ - public $filter; - /** - * @var boolean this property is overwritten to be false so that this validator will - * be applied when the value being validated is empty. - */ - public $skipOnEmpty = false; + /** + * @var callable the filter. This can be a global function name, anonymous function, etc. + * The function signature must be as follows, + * + * ~~~ + * function foo($value) {...return $newValue; } + * ~~~ + */ + public $filter; + /** + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. + */ + public $skipOnEmpty = false; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->filter === null) { - throw new InvalidConfigException('The "filter" property must be set.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->filter === null) { + throw new InvalidConfigException('The "filter" property must be set.'); + } + } - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $object->$attribute = call_user_func($this->filter, $object->$attribute); - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $object->$attribute = call_user_func($this->filter, $object->$attribute); + } } diff --git a/framework/validators/ImageValidator.php b/framework/validators/ImageValidator.php index fa9587d57a8..ebae6f17621 100644 --- a/framework/validators/ImageValidator.php +++ b/framework/validators/ImageValidator.php @@ -21,170 +21,172 @@ */ class ImageValidator extends FileValidator { - /** - * @var string the error message used when the uploaded file is not an image. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - */ - public $notImage; - /** - * @var integer the minimum width in pixels. - * Defaults to null, meaning no limit. - * @see underWidth - */ - public $minWidth; - /** - * @var integer the maximum width in pixels. - * Defaults to null, meaning no limit. - * @see overWidth - */ - public $maxWidth; - /** - * @var integer the minimum height in pixels. - * Defaults to null, meaning no limit. - * @see underHeight - */ - public $minHeight; - /** - * @var integer the maximum width in pixels. - * Defaults to null, meaning no limit. - * @see overWidth - */ - public $maxHeight; - /** - * @var array|string a list of file mime types that are allowed to be uploaded. - * This can be either an array or a string consisting of file mime types - * separated by space or comma (e.g. "image/jpeg, image/png"). - * Mime type names are case-insensitive. Defaults to null, meaning all mime types - * are allowed. - * @see wrongMimeType - */ - public $mimeTypes; - /** - * @var string the error message used when the image is under [[minWidth]]. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {limit}: the value of [[minWidth]] - */ - public $underWidth; - /** - * @var string the error message used when the image is over [[maxWidth]]. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {limit}: the value of [[maxWidth]] - */ - public $overWidth; - /** - * @var string the error message used when the image is under [[minHeight]]. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {limit}: the value of [[minHeight]] - */ - public $underHeight; - /** - * @var string the error message used when the image is over [[maxHeight]]. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {limit}: the value of [[maxHeight]] - */ - public $overHeight; - /** - * @var string the error message used when the file has an mime type - * that is not listed in [[mimeTypes]]. - * You may use the following tokens in the message: - * - * - {attribute}: the attribute name - * - {file}: the uploaded file name - * - {mimeTypes}: the value of [[mimeTypes]] - */ - public $wrongMimeType; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - - if ($this->notImage === null) { - $this->notImage = Yii::t('yii', 'The file "{file}" is not an image.'); - } - if ($this->underWidth === null) { - $this->underWidth = Yii::t('yii', 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); - } - if ($this->underHeight === null) { - $this->underHeight = Yii::t('yii', 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); - } - if ($this->overWidth === null) { - $this->overWidth = Yii::t('yii', 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); - } - if ($this->overHeight === null) { - $this->overHeight = Yii::t('yii', 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); - } - if ($this->wrongMimeType === null) { - $this->wrongMimeType = Yii::t('yii', 'Only files with these mimeTypes are allowed: {mimeTypes}.'); - } - if (!is_array($this->mimeTypes)) { - $this->mimeTypes = preg_split('/[\s,]+/', strtolower($this->mimeTypes), -1, PREG_SPLIT_NO_EMPTY); - } - } - - /** - * @inheritdoc - */ - protected function validateValue($file) - { - $result = parent::validateValue($file); - return empty($result) ? $this->validateImage($file) : $result; - } - - /** - * Validates an image file. - * @param UploadedFile $image uploaded file passed to check against a set of rules - * @return array|null the error message and the parameters to be inserted into the error message. - * Null should be returned if the data is valid. - */ - protected function validateImage($image) - { - if (!empty($this->mimeTypes) && !in_array(FileHelper::getMimeType($image->tempName), $this->mimeTypes, true)) { - return [$this->wrongMimeType, ['file' => $image->name, 'mimeTypes' => implode(', ', $this->mimeTypes)]]; - } - - if (false === ($imageInfo = getimagesize($image->tempName))) { - return [$this->notImage, ['file' => $image->name]]; - } - - list($width, $height, $type) = $imageInfo; - - if ($width == 0 || $height == 0) { - return [$this->notImage, ['file' => $image->name]]; - } - - if ($this->minWidth !== null && $width < $this->minWidth) { - return [$this->underWidth, ['file' => $image->name, 'limit' => $this->minWidth]]; - } - - if ($this->minHeight !== null && $height < $this->minHeight) { - return [$this->underHeight, ['file' => $image->name, 'limit' => $this->minHeight]]; - } - - if ($this->maxWidth !== null && $width > $this->maxWidth) { - return [$this->overWidth, ['file' => $image->name, 'limit' => $this->maxWidth]]; - } - - if ($this->maxHeight !== null && $height > $this->maxHeight) { - return [$this->overHeight, ['file' => $image->name, 'limit' => $this->maxHeight]]; - } - return null; - } + /** + * @var string the error message used when the uploaded file is not an image. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + */ + public $notImage; + /** + * @var integer the minimum width in pixels. + * Defaults to null, meaning no limit. + * @see underWidth + */ + public $minWidth; + /** + * @var integer the maximum width in pixels. + * Defaults to null, meaning no limit. + * @see overWidth + */ + public $maxWidth; + /** + * @var integer the minimum height in pixels. + * Defaults to null, meaning no limit. + * @see underHeight + */ + public $minHeight; + /** + * @var integer the maximum width in pixels. + * Defaults to null, meaning no limit. + * @see overWidth + */ + public $maxHeight; + /** + * @var array|string a list of file mime types that are allowed to be uploaded. + * This can be either an array or a string consisting of file mime types + * separated by space or comma (e.g. "image/jpeg, image/png"). + * Mime type names are case-insensitive. Defaults to null, meaning all mime types + * are allowed. + * @see wrongMimeType + */ + public $mimeTypes; + /** + * @var string the error message used when the image is under [[minWidth]]. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[minWidth]] + */ + public $underWidth; + /** + * @var string the error message used when the image is over [[maxWidth]]. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[maxWidth]] + */ + public $overWidth; + /** + * @var string the error message used when the image is under [[minHeight]]. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[minHeight]] + */ + public $underHeight; + /** + * @var string the error message used when the image is over [[maxHeight]]. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[maxHeight]] + */ + public $overHeight; + /** + * @var string the error message used when the file has an mime type + * that is not listed in [[mimeTypes]]. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {mimeTypes}: the value of [[mimeTypes]] + */ + public $wrongMimeType; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + + if ($this->notImage === null) { + $this->notImage = Yii::t('yii', 'The file "{file}" is not an image.'); + } + if ($this->underWidth === null) { + $this->underWidth = Yii::t('yii', 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); + } + if ($this->underHeight === null) { + $this->underHeight = Yii::t('yii', 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); + } + if ($this->overWidth === null) { + $this->overWidth = Yii::t('yii', 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); + } + if ($this->overHeight === null) { + $this->overHeight = Yii::t('yii', 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.'); + } + if ($this->wrongMimeType === null) { + $this->wrongMimeType = Yii::t('yii', 'Only files with these mimeTypes are allowed: {mimeTypes}.'); + } + if (!is_array($this->mimeTypes)) { + $this->mimeTypes = preg_split('/[\s,]+/', strtolower($this->mimeTypes), -1, PREG_SPLIT_NO_EMPTY); + } + } + + /** + * @inheritdoc + */ + protected function validateValue($file) + { + $result = parent::validateValue($file); + + return empty($result) ? $this->validateImage($file) : $result; + } + + /** + * Validates an image file. + * @param UploadedFile $image uploaded file passed to check against a set of rules + * @return array|null the error message and the parameters to be inserted into the error message. + * Null should be returned if the data is valid. + */ + protected function validateImage($image) + { + if (!empty($this->mimeTypes) && !in_array(FileHelper::getMimeType($image->tempName), $this->mimeTypes, true)) { + return [$this->wrongMimeType, ['file' => $image->name, 'mimeTypes' => implode(', ', $this->mimeTypes)]]; + } + + if (false === ($imageInfo = getimagesize($image->tempName))) { + return [$this->notImage, ['file' => $image->name]]; + } + + list($width, $height, $type) = $imageInfo; + + if ($width == 0 || $height == 0) { + return [$this->notImage, ['file' => $image->name]]; + } + + if ($this->minWidth !== null && $width < $this->minWidth) { + return [$this->underWidth, ['file' => $image->name, 'limit' => $this->minWidth]]; + } + + if ($this->minHeight !== null && $height < $this->minHeight) { + return [$this->underHeight, ['file' => $image->name, 'limit' => $this->minHeight]]; + } + + if ($this->maxWidth !== null && $width > $this->maxWidth) { + return [$this->overWidth, ['file' => $image->name, 'limit' => $this->maxWidth]]; + } + + if ($this->maxHeight !== null && $height > $this->maxHeight) { + return [$this->overHeight, ['file' => $image->name, 'limit' => $this->maxHeight]]; + } + + return null; + } } diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index b769e4e75f5..26bdc2dde72 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -24,61 +24,62 @@ */ class InlineValidator extends Validator { - /** - * @var string|\Closure an anonymous function or the name of a model class method that will be - * called to perform the actual validation. The signature of the method should be like the following: - * - * ~~~ - * function foo($attribute, $params) - * ~~~ - */ - public $method; - /** - * @var array additional parameters that are passed to the validation method - */ - public $params; - /** - * @var string|\Closure an anonymous function or the name of a model class method that returns the client validation code. - * The signature of the method should be like the following: - * - * ~~~ - * function foo($attribute, $params) - * { - * return "javascript"; - * } - * ~~~ - * - * where `$attribute` refers to the attribute name to be validated. - * - * Please refer to [[clientValidateAttribute()]] for details on how to return client validation code. - */ - public $clientValidate; + /** + * @var string|\Closure an anonymous function or the name of a model class method that will be + * called to perform the actual validation. The signature of the method should be like the following: + * + * ~~~ + * function foo($attribute, $params) + * ~~~ + */ + public $method; + /** + * @var array additional parameters that are passed to the validation method + */ + public $params; + /** + * @var string|\Closure an anonymous function or the name of a model class method that returns the client validation code. + * The signature of the method should be like the following: + * + * ~~~ + * function foo($attribute, $params) + * { + * return "javascript"; + * } + * ~~~ + * + * where `$attribute` refers to the attribute name to be validated. + * + * Please refer to [[clientValidateAttribute()]] for details on how to return client validation code. + */ + public $clientValidate; - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $method = $this->method; - if (is_string($method)) { - $method = [$object, $method]; - } - call_user_func($method, $attribute, $this->params); - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $method = $this->method; + if (is_string($method)) { + $method = [$object, $method]; + } + call_user_func($method, $attribute, $this->params); + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - if ($this->clientValidate !== null) { - $method = $this->clientValidate; - if (is_string($method)) { - $method = [$object, $method]; - } - return call_user_func($method, $attribute, $this->params); - } else { - return null; - } - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + if ($this->clientValidate !== null) { + $method = $this->clientValidate; + if (is_string($method)) { + $method = [$object, $method]; + } + + return call_user_func($method, $attribute, $this->params); + } else { + return null; + } + } } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index d18b5cc5cb0..ff3a71444db 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -23,130 +23,131 @@ */ class NumberValidator extends Validator { - /** - * @var boolean whether the attribute value can only be an integer. Defaults to false. - */ - public $integerOnly = false; - /** - * @var integer|float upper limit of the number. Defaults to null, meaning no upper limit. - */ - public $max; - /** - * @var integer|float lower limit of the number. Defaults to null, meaning no lower limit. - */ - public $min; - /** - * @var string user-defined error message used when the value is bigger than [[max]]. - */ - public $tooBig; - /** - * @var string user-defined error message used when the value is smaller than [[min]]. - */ - public $tooSmall; - /** - * @var string the regular expression for matching integers. - */ - public $integerPattern = '/^\s*[+-]?\d+\s*$/'; - /** - * @var string the regular expression for matching numbers. It defaults to a pattern - * that matches floating numbers with optional exponential part (e.g. -1.23e-10). - */ - public $numberPattern = '/^\s*[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\s*$/'; + /** + * @var boolean whether the attribute value can only be an integer. Defaults to false. + */ + public $integerOnly = false; + /** + * @var integer|float upper limit of the number. Defaults to null, meaning no upper limit. + */ + public $max; + /** + * @var integer|float lower limit of the number. Defaults to null, meaning no lower limit. + */ + public $min; + /** + * @var string user-defined error message used when the value is bigger than [[max]]. + */ + public $tooBig; + /** + * @var string user-defined error message used when the value is smaller than [[min]]. + */ + public $tooSmall; + /** + * @var string the regular expression for matching integers. + */ + public $integerPattern = '/^\s*[+-]?\d+\s*$/'; + /** + * @var string the regular expression for matching numbers. It defaults to a pattern + * that matches floating numbers with optional exponential part (e.g. -1.23e-10). + */ + public $numberPattern = '/^\s*[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\s*$/'; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->integerOnly ? Yii::t('yii', '{attribute} must be an integer.') + : Yii::t('yii', '{attribute} must be a number.'); + } + if ($this->min !== null && $this->tooSmall === null) { + $this->tooSmall = Yii::t('yii', '{attribute} must be no less than {min}.'); + } + if ($this->max !== null && $this->tooBig === null) { + $this->tooBig = Yii::t('yii', '{attribute} must be no greater than {max}.'); + } + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = $this->integerOnly ? Yii::t('yii', '{attribute} must be an integer.') - : Yii::t('yii', '{attribute} must be a number.'); - } - if ($this->min !== null && $this->tooSmall === null) { - $this->tooSmall = Yii::t('yii', '{attribute} must be no less than {min}.'); - } - if ($this->max !== null && $this->tooBig === null) { - $this->tooBig = Yii::t('yii', '{attribute} must be no greater than {max}.'); - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $value = $object->$attribute; + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $value = $object->$attribute; - if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - return; - } - $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; - if (!preg_match($pattern, "$value")) { - $this->addError($object, $attribute, $this->message); - } - if ($this->min !== null && $value < $this->min) { - $this->addError($object, $attribute, $this->tooSmall, ['min' => $this->min]); - } - if ($this->max !== null && $value > $this->max) { - $this->addError($object, $attribute, $this->tooBig, ['max' => $this->max]); - } - } + return; + } + $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; + if (!preg_match($pattern, "$value")) { + $this->addError($object, $attribute, $this->message); + } + if ($this->min !== null && $value < $this->min) { + $this->addError($object, $attribute, $this->tooSmall, ['min' => $this->min]); + } + if ($this->max !== null && $value > $this->max) { + $this->addError($object, $attribute, $this->tooBig, ['max' => $this->max]); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - if (is_array($value)) { - return [Yii::t('yii', '{attribute} is invalid.'), []]; - } - $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; - if (!preg_match($pattern, "$value")) { - return [$this->message, []]; - } elseif ($this->min !== null && $value < $this->min) { - return [$this->tooSmall, ['min' => $this->min]]; - } elseif ($this->max !== null && $value > $this->max) { - return [$this->tooBig, ['max' => $this->max]]; - } else { - return null; - } - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if (is_array($value)) { + return [Yii::t('yii', '{attribute} is invalid.'), []]; + } + $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; + if (!preg_match($pattern, "$value")) { + return [$this->message, []]; + } elseif ($this->min !== null && $value < $this->min) { + return [$this->tooSmall, ['min' => $this->min]]; + } elseif ($this->max !== null && $value > $this->max) { + return [$this->tooBig, ['max' => $this->max]]; + } else { + return null; + } + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $label = $object->getAttributeLabel($attribute); + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $label = $object->getAttributeLabel($attribute); - $options = [ - 'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern), - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $label, - ], Yii::$app->language), - ]; + $options = [ + 'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern), + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $label, + ], Yii::$app->language), + ]; - if ($this->min !== null) { - $options['min'] = $this->min; - $options['tooSmall'] = Yii::$app->getI18n()->format($this->tooSmall, [ - 'attribute' => $label, - 'min' => $this->min, - ], Yii::$app->language); - } - if ($this->max !== null) { - $options['max'] = $this->max; - $options['tooBig'] = Yii::$app->getI18n()->format($this->tooBig, [ - 'attribute' => $label, - 'max' => $this->max, - ], Yii::$app->language); - } - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } + if ($this->min !== null) { + $options['min'] = $this->min; + $options['tooSmall'] = Yii::$app->getI18n()->format($this->tooSmall, [ + 'attribute' => $label, + 'min' => $this->min, + ], Yii::$app->language); + } + if ($this->max !== null) { + $options['max'] = $this->max; + $options['tooBig'] = Yii::$app->getI18n()->format($this->tooBig, [ + 'attribute' => $label, + 'max' => $this->max, + ], Yii::$app->language); + } + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } - ValidationAsset::register($view); - return 'yii.validation.number(value, messages, ' . Json::encode($options) . ');'; - } + ValidationAsset::register($view); + + return 'yii.validation.number(value, messages, ' . Json::encode($options) . ');'; + } } diff --git a/framework/validators/PunycodeAsset.php b/framework/validators/PunycodeAsset.php index 5f6a4110cd8..1988da5e8a5 100644 --- a/framework/validators/PunycodeAsset.php +++ b/framework/validators/PunycodeAsset.php @@ -17,8 +17,8 @@ */ class PunycodeAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'punycode/punycode.js', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'punycode/punycode.js', + ]; } diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index e376a23407c..18f1864fd9f 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -22,65 +22,67 @@ */ class RangeValidator extends Validator { - /** - * @var array list of valid values that the attribute value should be among - */ - public $range; - /** - * @var boolean whether the comparison is strict (both type and value must be the same) - */ - public $strict = false; - /** - * @var boolean whether to invert the validation logic. Defaults to false. If set to true, - * the attribute value should NOT be among the list of values defined via [[range]]. - **/ - public $not = false; + /** + * @var array list of valid values that the attribute value should be among + */ + public $range; + /** + * @var boolean whether the comparison is strict (both type and value must be the same) + */ + public $strict = false; + /** + * @var boolean whether to invert the validation logic. Defaults to false. If set to true, + * the attribute value should NOT be among the list of values defined via [[range]]. + **/ + public $not = false; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be set.'); - } - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} is invalid.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!is_array($this->range)) { + throw new InvalidConfigException('The "range" property must be set.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} is invalid.'); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - $valid = !$this->not && in_array($value, $this->range, $this->strict) - || $this->not && !in_array($value, $this->range, $this->strict); - return $valid ? null : [$this->message, []]; - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + $valid = !$this->not && in_array($value, $this->range, $this->strict) + || $this->not && !in_array($value, $this->range, $this->strict); - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $range = []; - foreach ($this->range as $value) { - $range[] = (string)$value; - } - $options = [ - 'range' => $range, - 'not' => $this->not, - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - ], Yii::$app->language), - ]; - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } + return $valid ? null : [$this->message, []]; + } - ValidationAsset::register($view); - return 'yii.validation.range(value, messages, ' . json_encode($options) . ');'; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $range = []; + foreach ($this->range as $value) { + $range[] = (string) $value; + } + $options = [ + 'range' => $range, + 'not' => $this->not, + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + ], Yii::$app->language), + ]; + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + + ValidationAsset::register($view); + + return 'yii.validation.range(value, messages, ' . json_encode($options) . ');'; + } } diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index 68dc9e545a0..d46344958c8 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -22,72 +22,74 @@ */ class RegularExpressionValidator extends Validator { - /** - * @var string the regular expression to be matched with - */ - public $pattern; - /** - * @var boolean whether to invert the validation logic. Defaults to false. If set to true, - * the regular expression defined via [[pattern]] should NOT match the attribute value. - **/ - public $not = false; + /** + * @var string the regular expression to be matched with + */ + public $pattern; + /** + * @var boolean whether to invert the validation logic. Defaults to false. If set to true, + * the regular expression defined via [[pattern]] should NOT match the attribute value. + **/ + public $not = false; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->pattern === null) { - throw new InvalidConfigException('The "pattern" property must be set.'); - } - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} is invalid.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->pattern === null) { + throw new InvalidConfigException('The "pattern" property must be set.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} is invalid.'); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - $valid = !is_array($value) && - (!$this->not && preg_match($this->pattern, $value) - || $this->not && !preg_match($this->pattern, $value)); - return $valid ? null : [$this->message, []]; - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + $valid = !is_array($value) && + (!$this->not && preg_match($this->pattern, $value) + || $this->not && !preg_match($this->pattern, $value)); - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $pattern = $this->pattern; - $pattern = preg_replace('/\\\\x\{?([0-9a-fA-F]+)\}?/', '\u$1', $pattern); - $deliminator = substr($pattern, 0, 1); - $pos = strrpos($pattern, $deliminator, 1); - $flag = substr($pattern, $pos + 1); - if ($deliminator !== '/') { - $pattern = '/' . str_replace('/', '\\/', substr($pattern, 1, $pos - 1)) . '/'; - } else { - $pattern = substr($pattern, 0, $pos + 1); - } - if (!empty($flag)) { - $pattern .= preg_replace('/[^igm]/', '', $flag); - } + return $valid ? null : [$this->message, []]; + } - $options = [ - 'pattern' => new JsExpression($pattern), - 'not' => $this->not, - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - ], Yii::$app->language), - ]; - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $pattern = $this->pattern; + $pattern = preg_replace('/\\\\x\{?([0-9a-fA-F]+)\}?/', '\u$1', $pattern); + $deliminator = substr($pattern, 0, 1); + $pos = strrpos($pattern, $deliminator, 1); + $flag = substr($pattern, $pos + 1); + if ($deliminator !== '/') { + $pattern = '/' . str_replace('/', '\\/', substr($pattern, 1, $pos - 1)) . '/'; + } else { + $pattern = substr($pattern, 0, $pos + 1); + } + if (!empty($flag)) { + $pattern .= preg_replace('/[^igm]/', '', $flag); + } - ValidationAsset::register($view); - return 'yii.validation.regularExpression(value, messages, ' . Json::encode($options) . ');'; - } + $options = [ + 'pattern' => new JsExpression($pattern), + 'not' => $this->not, + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + ], Yii::$app->language), + ]; + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + + ValidationAsset::register($view); + + return 'yii.validation.regularExpression(value, messages, ' . Json::encode($options) . ');'; + } } diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index 72618f9d7ad..2bbe27dc0ac 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -17,94 +17,95 @@ */ class RequiredValidator extends Validator { - /** - * @var boolean whether to skip this validator if the value being validated is empty. - */ - public $skipOnEmpty = false; - /** - * @var mixed the desired value that the attribute must have. - * If this is null, the validator will validate that the specified attribute is not empty. - * If this is set as a value that is not null, the validator will validate that - * the attribute has a value that is the same as this property value. - * Defaults to null. - * @see strict - */ - public $requiredValue; - /** - * @var boolean whether the comparison between the attribute value and [[requiredValue]] is strict. - * When this is true, both the values and types must match. - * Defaults to false, meaning only the values need to match. - * Note that when [[requiredValue]] is null, if this property is true, the validator will check - * if the attribute value is null; If this property is false, the validator will call [[isEmpty]] - * to check if the attribute value is empty. - */ - public $strict = false; - /** - * @var string the user-defined error message. It may contain the following placeholders which - * will be replaced accordingly by the validator: - * - * - `{attribute}`: the label of the attribute being validated - * - `{value}`: the value of the attribute being validated - * - `{requiredValue}`: the value of [[requiredValue]] - */ - public $message; + /** + * @var boolean whether to skip this validator if the value being validated is empty. + */ + public $skipOnEmpty = false; + /** + * @var mixed the desired value that the attribute must have. + * If this is null, the validator will validate that the specified attribute is not empty. + * If this is set as a value that is not null, the validator will validate that + * the attribute has a value that is the same as this property value. + * Defaults to null. + * @see strict + */ + public $requiredValue; + /** + * @var boolean whether the comparison between the attribute value and [[requiredValue]] is strict. + * When this is true, both the values and types must match. + * Defaults to false, meaning only the values need to match. + * Note that when [[requiredValue]] is null, if this property is true, the validator will check + * if the attribute value is null; If this property is false, the validator will call [[isEmpty]] + * to check if the attribute value is empty. + */ + public $strict = false; + /** + * @var string the user-defined error message. It may contain the following placeholders which + * will be replaced accordingly by the validator: + * + * - `{attribute}`: the label of the attribute being validated + * - `{value}`: the value of the attribute being validated + * - `{requiredValue}`: the value of [[requiredValue]] + */ + public $message; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = $this->requiredValue === null ? Yii::t('yii', '{attribute} cannot be blank.') - : Yii::t('yii', '{attribute} must be "{requiredValue}".'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->requiredValue === null ? Yii::t('yii', '{attribute} cannot be blank.') + : Yii::t('yii', '{attribute} must be "{requiredValue}".'); + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - if ($this->requiredValue === null) { - if ($this->strict && $value !== null || !$this->strict && !$this->isEmpty($value, true)) { - return null; - } - } elseif (!$this->strict && $value == $this->requiredValue || $this->strict && $value === $this->requiredValue) { - return null; - } - if ($this->requiredValue === null) { - return [$this->message, []]; - } else { - return [$this->message, [ - 'requiredValue' => $this->requiredValue, - ]]; - } - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if ($this->requiredValue === null) { + if ($this->strict && $value !== null || !$this->strict && !$this->isEmpty($value, true)) { + return null; + } + } elseif (!$this->strict && $value == $this->requiredValue || $this->strict && $value === $this->requiredValue) { + return null; + } + if ($this->requiredValue === null) { + return [$this->message, []]; + } else { + return [$this->message, [ + 'requiredValue' => $this->requiredValue, + ]]; + } + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $options = []; - if ($this->requiredValue !== null) { - $options['message'] = Yii::$app->getI18n()->format($this->message, [ - 'requiredValue' => $this->requiredValue, - ], Yii::$app->language); - $options['requiredValue'] = $this->requiredValue; - } else { - $options['message'] = $this->message; - } - if ($this->strict) { - $options['strict'] = 1; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $options = []; + if ($this->requiredValue !== null) { + $options['message'] = Yii::$app->getI18n()->format($this->message, [ + 'requiredValue' => $this->requiredValue, + ], Yii::$app->language); + $options['requiredValue'] = $this->requiredValue; + } else { + $options['message'] = $this->message; + } + if ($this->strict) { + $options['strict'] = 1; + } - $options['message'] = Yii::$app->getI18n()->format($options['message'], [ - 'attribute' => $object->getAttributeLabel($attribute), - ], Yii::$app->language); + $options['message'] = Yii::$app->getI18n()->format($options['message'], [ + 'attribute' => $object->getAttributeLabel($attribute), + ], Yii::$app->language); - ValidationAsset::register($view); - return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; - } + ValidationAsset::register($view); + + return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; + } } diff --git a/framework/validators/SafeValidator.php b/framework/validators/SafeValidator.php index 25da899ce14..fcb8440ec51 100644 --- a/framework/validators/SafeValidator.php +++ b/framework/validators/SafeValidator.php @@ -15,10 +15,10 @@ */ class SafeValidator extends Validator { - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + } } diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 6deea397ac1..14a30eb0c49 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -19,168 +19,169 @@ */ class StringValidator extends Validator { - /** - * @var integer|array specifies the length limit of the value to be validated. - * This can be specified in one of the following forms: - * - * - an integer: the exact length that the value should be of; - * - an array of one element: the minimum length that the value should be of. For example, `[8]`. - * This will overwrite [[min]]. - * - an array of two elements: the minimum and maximum lengths that the value should be of. - * For example, `[8, 128]`. This will overwrite both [[min]] and [[max]]. - */ - public $length; - /** - * @var integer maximum length. If not set, it means no maximum length limit. - */ - public $max; - /** - * @var integer minimum length. If not set, it means no minimum length limit. - */ - public $min; - /** - * @var string user-defined error message used when the value is not a string - */ - public $message; - /** - * @var string user-defined error message used when the length of the value is smaller than [[min]]. - */ - public $tooShort; - /** - * @var string user-defined error message used when the length of the value is greater than [[max]]. - */ - public $tooLong; - /** - * @var string user-defined error message used when the length of the value is not equal to [[length]]. - */ - public $notEqual; - /** - * @var string the encoding of the string value to be validated (e.g. 'UTF-8'). - * If this property is not set, [[\yii\base\Application::charset]] will be used. - */ - public $encoding; - - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (is_array($this->length)) { - if (isset($this->length[0])) { - $this->min = $this->length[0]; - } - if (isset($this->length[1])) { - $this->max = $this->length[1]; - } - $this->length = null; - } - if ($this->encoding === null) { - $this->encoding = Yii::$app->charset; - } - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} must be a string.'); - } - if ($this->min !== null && $this->tooShort === null) { - $this->tooShort = Yii::t('yii', '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.'); - } - if ($this->max !== null && $this->tooLong === null) { - $this->tooLong = Yii::t('yii', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.'); - } - if ($this->length !== null && $this->notEqual === null) { - $this->notEqual = Yii::t('yii', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.'); - } - } - - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $value = $object->$attribute; - - if (!is_string($value)) { - $this->addError($object, $attribute, $this->message); - return; - } - - $length = mb_strlen($value, $this->encoding); - - if ($this->min !== null && $length < $this->min) { - $this->addError($object, $attribute, $this->tooShort, ['min' => $this->min]); - } - if ($this->max !== null && $length > $this->max) { - $this->addError($object, $attribute, $this->tooLong, ['max' => $this->max]); - } - if ($this->length !== null && $length !== $this->length) { - $this->addError($object, $attribute, $this->notEqual, ['length' => $this->length]); - } - } - - /** - * @inheritdoc - */ - protected function validateValue($value) - { - if (!is_string($value)) { - return [$this->message, []]; - } - - $length = mb_strlen($value, $this->encoding); - - if ($this->min !== null && $length < $this->min) { - return [$this->tooShort, ['min' => $this->min]]; - } - if ($this->max !== null && $length > $this->max) { - return [$this->tooLong, ['max' => $this->max]]; - } - if ($this->length !== null && $length !== $this->length) { - return [$this->notEqual, ['length' => $this->length]]; - } - - return null; - } - - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - $label = $object->getAttributeLabel($attribute); - - $options = [ - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $label, - ], Yii::$app->language), - ]; - - if ($this->min !== null) { - $options['min'] = $this->min; - $options['tooShort'] = Yii::$app->getI18n()->format($this->tooShort, [ - 'attribute' => $label, - 'min' => $this->min, - ], Yii::$app->language); - } - if ($this->max !== null) { - $options['max'] = $this->max; - $options['tooLong'] = Yii::$app->getI18n()->format($this->tooLong, [ - 'attribute' => $label, - 'max' => $this->max, - ], Yii::$app->language); - } - if ($this->length !== null) { - $options['is'] = $this->length; - $options['notEqual'] = Yii::$app->getI18n()->format($this->notEqual, [ - 'attribute' => $label, - 'length' => $this->length, - ], Yii::$app->language); - } - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } - - ValidationAsset::register($view); - return 'yii.validation.string(value, messages, ' . json_encode($options) . ');'; - } + /** + * @var integer|array specifies the length limit of the value to be validated. + * This can be specified in one of the following forms: + * + * - an integer: the exact length that the value should be of; + * - an array of one element: the minimum length that the value should be of. For example, `[8]`. + * This will overwrite [[min]]. + * - an array of two elements: the minimum and maximum lengths that the value should be of. + * For example, `[8, 128]`. This will overwrite both [[min]] and [[max]]. + */ + public $length; + /** + * @var integer maximum length. If not set, it means no maximum length limit. + */ + public $max; + /** + * @var integer minimum length. If not set, it means no minimum length limit. + */ + public $min; + /** + * @var string user-defined error message used when the value is not a string + */ + public $message; + /** + * @var string user-defined error message used when the length of the value is smaller than [[min]]. + */ + public $tooShort; + /** + * @var string user-defined error message used when the length of the value is greater than [[max]]. + */ + public $tooLong; + /** + * @var string user-defined error message used when the length of the value is not equal to [[length]]. + */ + public $notEqual; + /** + * @var string the encoding of the string value to be validated (e.g. 'UTF-8'). + * If this property is not set, [[\yii\base\Application::charset]] will be used. + */ + public $encoding; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (is_array($this->length)) { + if (isset($this->length[0])) { + $this->min = $this->length[0]; + } + if (isset($this->length[1])) { + $this->max = $this->length[1]; + } + $this->length = null; + } + if ($this->encoding === null) { + $this->encoding = Yii::$app->charset; + } + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} must be a string.'); + } + if ($this->min !== null && $this->tooShort === null) { + $this->tooShort = Yii::t('yii', '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.'); + } + if ($this->max !== null && $this->tooLong === null) { + $this->tooLong = Yii::t('yii', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.'); + } + if ($this->length !== null && $this->notEqual === null) { + $this->notEqual = Yii::t('yii', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.'); + } + } + + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $value = $object->$attribute; + + if (!is_string($value)) { + $this->addError($object, $attribute, $this->message); + + return; + } + + $length = mb_strlen($value, $this->encoding); + + if ($this->min !== null && $length < $this->min) { + $this->addError($object, $attribute, $this->tooShort, ['min' => $this->min]); + } + if ($this->max !== null && $length > $this->max) { + $this->addError($object, $attribute, $this->tooLong, ['max' => $this->max]); + } + if ($this->length !== null && $length !== $this->length) { + $this->addError($object, $attribute, $this->notEqual, ['length' => $this->length]); + } + } + + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if (!is_string($value)) { + return [$this->message, []]; + } + + $length = mb_strlen($value, $this->encoding); + + if ($this->min !== null && $length < $this->min) { + return [$this->tooShort, ['min' => $this->min]]; + } + if ($this->max !== null && $length > $this->max) { + return [$this->tooLong, ['max' => $this->max]]; + } + if ($this->length !== null && $length !== $this->length) { + return [$this->notEqual, ['length' => $this->length]]; + } + + return null; + } + + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + $label = $object->getAttributeLabel($attribute); + + $options = [ + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $label, + ], Yii::$app->language), + ]; + + if ($this->min !== null) { + $options['min'] = $this->min; + $options['tooShort'] = Yii::$app->getI18n()->format($this->tooShort, [ + 'attribute' => $label, + 'min' => $this->min, + ], Yii::$app->language); + } + if ($this->max !== null) { + $options['max'] = $this->max; + $options['tooLong'] = Yii::$app->getI18n()->format($this->tooLong, [ + 'attribute' => $label, + 'max' => $this->max, + ], Yii::$app->language); + } + if ($this->length !== null) { + $options['is'] = $this->length; + $options['notEqual'] = Yii::$app->getI18n()->format($this->notEqual, [ + 'attribute' => $label, + 'length' => $this->length, + ], Yii::$app->language); + } + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + + ValidationAsset::register($view); + + return 'yii.validation.string(value, messages, ' . json_encode($options) . ');'; + } } diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 0b311d615fa..46b52e897b2 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -36,101 +36,102 @@ */ class UniqueValidator extends Validator { - /** - * @var string the name of the ActiveRecord class that should be used to validate the uniqueness - * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated. - * @see targetAttribute - */ - public $targetClass; - /** - * @var string|array the name of the ActiveRecord attribute that should be used to - * validate the uniqueness of the current attribute value. If not set, it will use the name - * of the attribute currently being validated. You may use an array to validate the uniqueness - * of multiple columns at the same time. The array values are the attributes that will be - * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated. - * If the key and the value are the same, you can just specify the value. - */ - public $targetAttribute; - /** - * @var string|array|\Closure additional filter to be applied to the DB query used to check the uniqueness of the attribute value. - * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]] - * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` - * is the [[\yii\db\Query|Query]] object that you can modify in the function. - */ - public $filter; + /** + * @var string the name of the ActiveRecord class that should be used to validate the uniqueness + * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated. + * @see targetAttribute + */ + public $targetClass; + /** + * @var string|array the name of the ActiveRecord attribute that should be used to + * validate the uniqueness of the current attribute value. If not set, it will use the name + * of the attribute currently being validated. You may use an array to validate the uniqueness + * of multiple columns at the same time. The array values are the attributes that will be + * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated. + * If the key and the value are the same, you can just specify the value. + */ + public $targetAttribute; + /** + * @var string|array|\Closure additional filter to be applied to the DB query used to check the uniqueness of the attribute value. + * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]] + * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query` + * is the [[\yii\db\Query|Query]] object that you can modify in the function. + */ + public $filter; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} "{value}" has already been taken.'); + } + } - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - /** @var ActiveRecordInterface $targetClass */ - $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass; - $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + /** @var ActiveRecordInterface $targetClass */ + $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass; + $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; - if (is_array($targetAttribute)) { - $params = []; - foreach ($targetAttribute as $k => $v) { - $params[$v] = is_integer($k) ? $object->$v : $object->$k; - } - } else { - $params = [$targetAttribute => $object->$attribute]; - } + if (is_array($targetAttribute)) { + $params = []; + foreach ($targetAttribute as $k => $v) { + $params[$v] = is_integer($k) ? $object->$v : $object->$k; + } + } else { + $params = [$targetAttribute => $object->$attribute]; + } - foreach ($params as $value) { - if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - return; - } - } + foreach ($params as $value) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - $query = $targetClass::find(); - $query->where($params); + return; + } + } - if ($this->filter instanceof \Closure) { - call_user_func($this->filter, $query); - } elseif ($this->filter !== null) { - $query->andWhere($this->filter); - } + $query = $targetClass::find(); + $query->where($params); - if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) { - // if current $object isn't in the database yet then it's OK just to call exists() - $exists = $query->exists(); - } else { - // if current $object is in the database already we can't use exists() - /** @var ActiveRecordInterface[] $objects */ - $objects = $query->limit(2)->all(); - $n = count($objects); - if ($n === 1) { - $keys = array_keys($params); - $pks = $targetClass::primaryKey(); - sort($keys); - sort($pks); - if ($keys === $pks) { - // primary key is modified and not unique - $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); - } else { - // non-primary key, need to exclude the current record based on PK - $exists = $objects[0]->getPrimaryKey() != $object->getOldPrimaryKey(); - } - } else { - $exists = $n > 1; - } - } + if ($this->filter instanceof \Closure) { + call_user_func($this->filter, $query); + } elseif ($this->filter !== null) { + $query->andWhere($this->filter); + } - if ($exists) { - $this->addError($object, $attribute, $this->message); - } - } + if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) { + // if current $object isn't in the database yet then it's OK just to call exists() + $exists = $query->exists(); + } else { + // if current $object is in the database already we can't use exists() + /** @var ActiveRecordInterface[] $objects */ + $objects = $query->limit(2)->all(); + $n = count($objects); + if ($n === 1) { + $keys = array_keys($params); + $pks = $targetClass::primaryKey(); + sort($keys); + sort($pks); + if ($keys === $pks) { + // primary key is modified and not unique + $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); + } else { + // non-primary key, need to exclude the current record based on PK + $exists = $objects[0]->getPrimaryKey() != $object->getOldPrimaryKey(); + } + } else { + $exists = $n > 1; + } + } + + if ($exists) { + $this->addError($object, $attribute, $this->message); + } + } } diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index feaf145abbd..cf31642e543 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -23,119 +23,121 @@ */ class UrlValidator extends Validator { - /** - * @var string the regular expression used to validate the attribute value. - * The pattern may contain a `{schemes}` token that will be replaced - * by a regular expression which represents the [[validSchemes]]. - */ - public $pattern = '/^{schemes}:\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)/i'; - /** - * @var array list of URI schemes which should be considered valid. By default, http and https - * are considered to be valid schemes. - **/ - public $validSchemes = ['http', 'https']; - /** - * @var string the default URI scheme. If the input doesn't contain the scheme part, the default - * scheme will be prepended to it (thus changing the input). Defaults to null, meaning a URL must - * contain the scheme part. - **/ - public $defaultScheme; - /** - * @var boolean whether validation process should take into account IDN (internationalized - * domain names). Defaults to false meaning that validation of URLs containing IDN will always - * fail. Note that in order to use IDN validation you have to install and enable `intl` PHP - * extension, otherwise an exception would be thrown. - */ - public $enableIDN = false; + /** + * @var string the regular expression used to validate the attribute value. + * The pattern may contain a `{schemes}` token that will be replaced + * by a regular expression which represents the [[validSchemes]]. + */ + public $pattern = '/^{schemes}:\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)/i'; + /** + * @var array list of URI schemes which should be considered valid. By default, http and https + * are considered to be valid schemes. + **/ + public $validSchemes = ['http', 'https']; + /** + * @var string the default URI scheme. If the input doesn't contain the scheme part, the default + * scheme will be prepended to it (thus changing the input). Defaults to null, meaning a URL must + * contain the scheme part. + **/ + public $defaultScheme; + /** + * @var boolean whether validation process should take into account IDN (internationalized + * domain names). Defaults to false meaning that validation of URLs containing IDN will always + * fail. Note that in order to use IDN validation you have to install and enable `intl` PHP + * extension, otherwise an exception would be thrown. + */ + public $enableIDN = false; - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if ($this->enableIDN && !function_exists('idn_to_ascii')) { - throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.'); - } - if ($this->message === null) { - $this->message = Yii::t('yii', '{attribute} is not a valid URL.'); - } - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->enableIDN && !function_exists('idn_to_ascii')) { + throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii', '{attribute} is not a valid URL.'); + } + } - /** - * @inheritdoc - */ - public function validateAttribute($object, $attribute) - { - $value = $object->$attribute; - $result = $this->validateValue($value); - if (!empty($result)) { - $this->addError($object, $attribute, $result[0], $result[1]); - } elseif ($this->defaultScheme !== null && strpos($value, '://') === false) { - $object->$attribute = $this->defaultScheme . '://' . $value; - } - } + /** + * @inheritdoc + */ + public function validateAttribute($object, $attribute) + { + $value = $object->$attribute; + $result = $this->validateValue($value); + if (!empty($result)) { + $this->addError($object, $attribute, $result[0], $result[1]); + } elseif ($this->defaultScheme !== null && strpos($value, '://') === false) { + $object->$attribute = $this->defaultScheme . '://' . $value; + } + } - /** - * @inheritdoc - */ - protected function validateValue($value) - { - // make sure the length is limited to avoid DOS attacks - if (is_string($value) && strlen($value) < 2000) { - if ($this->defaultScheme !== null && strpos($value, '://') === false) { - $value = $this->defaultScheme . '://' . $value; - } + /** + * @inheritdoc + */ + protected function validateValue($value) + { + // make sure the length is limited to avoid DOS attacks + if (is_string($value) && strlen($value) < 2000) { + if ($this->defaultScheme !== null && strpos($value, '://') === false) { + $value = $this->defaultScheme . '://' . $value; + } - if (strpos($this->pattern, '{schemes}') !== false) { - $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); - } else { - $pattern = $this->pattern; - } + if (strpos($this->pattern, '{schemes}') !== false) { + $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); + } else { + $pattern = $this->pattern; + } - if ($this->enableIDN) { - $value = preg_replace_callback('/:\/\/([^\/]+)/', function ($matches) { - return '://' . idn_to_ascii($matches[1]); - }, $value); - } + if ($this->enableIDN) { + $value = preg_replace_callback('/:\/\/([^\/]+)/', function ($matches) { + return '://' . idn_to_ascii($matches[1]); + }, $value); + } - if (preg_match($pattern, $value)) { - return null; - } - } - return [$this->message, []]; - } + if (preg_match($pattern, $value)) { + return null; + } + } - /** - * @inheritdoc - */ - public function clientValidateAttribute($object, $attribute, $view) - { - if (strpos($this->pattern, '{schemes}') !== false) { - $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); - } else { - $pattern = $this->pattern; - } + return [$this->message, []]; + } - $options = [ - 'pattern' => new JsExpression($pattern), - 'message' => Yii::$app->getI18n()->format($this->message, [ - 'attribute' => $object->getAttributeLabel($attribute), - ], Yii::$app->language), - 'enableIDN' => (boolean)$this->enableIDN, - ]; - if ($this->skipOnEmpty) { - $options['skipOnEmpty'] = 1; - } - if ($this->defaultScheme !== null) { - $options['defaultScheme'] = $this->defaultScheme; - } + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + if (strpos($this->pattern, '{schemes}') !== false) { + $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); + } else { + $pattern = $this->pattern; + } - ValidationAsset::register($view); - if ($this->enableIDN) { - PunycodeAsset::register($view); - } - return 'yii.validation.url(value, messages, ' . Json::encode($options) . ');'; - } + $options = [ + 'pattern' => new JsExpression($pattern), + 'message' => Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $object->getAttributeLabel($attribute), + ], Yii::$app->language), + 'enableIDN' => (boolean) $this->enableIDN, + ]; + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + if ($this->defaultScheme !== null) { + $options['defaultScheme'] = $this->defaultScheme; + } + + ValidationAsset::register($view); + if ($this->enableIDN) { + PunycodeAsset::register($view); + } + + return 'yii.validation.url(value, messages, ' . Json::encode($options) . ');'; + } } diff --git a/framework/validators/ValidationAsset.php b/framework/validators/ValidationAsset.php index e9bb79d305c..a25acffd625 100644 --- a/framework/validators/ValidationAsset.php +++ b/framework/validators/ValidationAsset.php @@ -17,11 +17,11 @@ */ class ValidationAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'yii.validation.js', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'yii.validation.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index fac370650ba..5a3b4703919 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -47,266 +47,266 @@ */ class Validator extends Component { - /** - * @var array list of built-in validators (name => class or configuration) - */ - public static $builtInValidators = [ - 'boolean' => 'yii\validators\BooleanValidator', - 'captcha' => 'yii\captcha\CaptchaValidator', - 'compare' => 'yii\validators\CompareValidator', - 'date' => 'yii\validators\DateValidator', - 'default' => 'yii\validators\DefaultValueValidator', - 'double' => 'yii\validators\NumberValidator', - 'email' => 'yii\validators\EmailValidator', - 'exist' => 'yii\validators\ExistValidator', - 'file' => 'yii\validators\FileValidator', - 'filter' => 'yii\validators\FilterValidator', - 'image' => 'yii\validators\ImageValidator', - 'in' => 'yii\validators\RangeValidator', - 'integer' => [ - 'class' => 'yii\validators\NumberValidator', - 'integerOnly' => true, - ], - 'match' => 'yii\validators\RegularExpressionValidator', - 'number' => 'yii\validators\NumberValidator', - 'required' => 'yii\validators\RequiredValidator', - 'safe' => 'yii\validators\SafeValidator', - 'string' => 'yii\validators\StringValidator', - 'trim' => [ - 'class' => 'yii\validators\FilterValidator', - 'filter' => 'trim', - ], - 'unique' => 'yii\validators\UniqueValidator', - 'url' => 'yii\validators\UrlValidator', - ]; + /** + * @var array list of built-in validators (name => class or configuration) + */ + public static $builtInValidators = [ + 'boolean' => 'yii\validators\BooleanValidator', + 'captcha' => 'yii\captcha\CaptchaValidator', + 'compare' => 'yii\validators\CompareValidator', + 'date' => 'yii\validators\DateValidator', + 'default' => 'yii\validators\DefaultValueValidator', + 'double' => 'yii\validators\NumberValidator', + 'email' => 'yii\validators\EmailValidator', + 'exist' => 'yii\validators\ExistValidator', + 'file' => 'yii\validators\FileValidator', + 'filter' => 'yii\validators\FilterValidator', + 'image' => 'yii\validators\ImageValidator', + 'in' => 'yii\validators\RangeValidator', + 'integer' => [ + 'class' => 'yii\validators\NumberValidator', + 'integerOnly' => true, + ], + 'match' => 'yii\validators\RegularExpressionValidator', + 'number' => 'yii\validators\NumberValidator', + 'required' => 'yii\validators\RequiredValidator', + 'safe' => 'yii\validators\SafeValidator', + 'string' => 'yii\validators\StringValidator', + 'trim' => [ + 'class' => 'yii\validators\FilterValidator', + 'filter' => 'trim', + ], + 'unique' => 'yii\validators\UniqueValidator', + 'url' => 'yii\validators\UrlValidator', + ]; - /** - * @var array|string attributes to be validated by this validator. For multiple attributes, - * please specify them as an array; for single attribute, you may use either a string or an array. - */ - public $attributes = []; - /** - * @var string the user-defined error message. It may contain the following placeholders which - * will be replaced accordingly by the validator: - * - * - `{attribute}`: the label of the attribute being validated - * - `{value}`: the value of the attribute being validated - */ - public $message; - /** - * @var array|string scenarios that the validator can be applied to. For multiple scenarios, - * please specify them as an array; for single scenario, you may use either a string or an array. - */ - public $on = []; - /** - * @var array|string scenarios that the validator should not be applied to. For multiple scenarios, - * please specify them as an array; for single scenario, you may use either a string or an array. - */ - public $except = []; - /** - * @var boolean whether this validation rule should be skipped if the attribute being validated - * already has some validation error according to some previous rules. Defaults to true. - */ - public $skipOnError = true; - /** - * @var boolean whether this validation rule should be skipped if the attribute value - * is null or an empty string. - */ - public $skipOnEmpty = true; - /** - * @var boolean whether to enable client-side validation for this validator. - * The actual client-side validation is done via the JavaScript code returned - * by [[clientValidateAttribute()]]. If that method returns null, even if this property - * is true, no client-side validation will be done by this validator. - */ - public $enableClientValidation = true; + /** + * @var array|string attributes to be validated by this validator. For multiple attributes, + * please specify them as an array; for single attribute, you may use either a string or an array. + */ + public $attributes = []; + /** + * @var string the user-defined error message. It may contain the following placeholders which + * will be replaced accordingly by the validator: + * + * - `{attribute}`: the label of the attribute being validated + * - `{value}`: the value of the attribute being validated + */ + public $message; + /** + * @var array|string scenarios that the validator can be applied to. For multiple scenarios, + * please specify them as an array; for single scenario, you may use either a string or an array. + */ + public $on = []; + /** + * @var array|string scenarios that the validator should not be applied to. For multiple scenarios, + * please specify them as an array; for single scenario, you may use either a string or an array. + */ + public $except = []; + /** + * @var boolean whether this validation rule should be skipped if the attribute being validated + * already has some validation error according to some previous rules. Defaults to true. + */ + public $skipOnError = true; + /** + * @var boolean whether this validation rule should be skipped if the attribute value + * is null or an empty string. + */ + public $skipOnEmpty = true; + /** + * @var boolean whether to enable client-side validation for this validator. + * The actual client-side validation is done via the JavaScript code returned + * by [[clientValidateAttribute()]]. If that method returns null, even if this property + * is true, no client-side validation will be done by this validator. + */ + public $enableClientValidation = true; + /** + * Creates a validator object. + * @param mixed $type the validator type. This can be a built-in validator name, + * a method name of the model class, an anonymous function, or a validator class name. + * @param \yii\base\Model $object the data object to be validated. + * @param array|string $attributes list of attributes to be validated. This can be either an array of + * the attribute names or a string of comma-separated attribute names. + * @param array $params initial values to be applied to the validator properties + * @return Validator the validator + */ + public static function createValidator($type, $object, $attributes, $params = []) + { + $params['attributes'] = $attributes; - /** - * Creates a validator object. - * @param mixed $type the validator type. This can be a built-in validator name, - * a method name of the model class, an anonymous function, or a validator class name. - * @param \yii\base\Model $object the data object to be validated. - * @param array|string $attributes list of attributes to be validated. This can be either an array of - * the attribute names or a string of comma-separated attribute names. - * @param array $params initial values to be applied to the validator properties - * @return Validator the validator - */ - public static function createValidator($type, $object, $attributes, $params = []) - { - $params['attributes'] = $attributes; + if ($type instanceof \Closure || $object->hasMethod($type)) { + // method-based validator + $params['class'] = __NAMESPACE__ . '\InlineValidator'; + $params['method'] = $type; + } else { + if (isset(static::$builtInValidators[$type])) { + $type = static::$builtInValidators[$type]; + } + if (is_array($type)) { + foreach ($type as $name => $value) { + $params[$name] = $value; + } + } else { + $params['class'] = $type; + } + } - if ($type instanceof \Closure || $object->hasMethod($type)) { - // method-based validator - $params['class'] = __NAMESPACE__ . '\InlineValidator'; - $params['method'] = $type; - } else { - if (isset(static::$builtInValidators[$type])) { - $type = static::$builtInValidators[$type]; - } - if (is_array($type)) { - foreach ($type as $name => $value) { - $params[$name] = $value; - } - } else { - $params['class'] = $type; - } - } + return Yii::createObject($params); + } - return Yii::createObject($params); - } + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->attributes = (array) $this->attributes; + $this->on = (array) $this->on; + $this->except = (array) $this->except; + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - $this->attributes = (array)$this->attributes; - $this->on = (array)$this->on; - $this->except = (array)$this->except; - } + /** + * Validates the specified object. + * @param \yii\base\Model $object the data object being validated + * @param array|null $attributes the list of attributes to be validated. + * Note that if an attribute is not associated with the validator, + * it will be ignored. + * If this parameter is null, every attribute listed in [[attributes]] will be validated. + */ + public function validateAttributes($object, $attributes = null) + { + if (is_array($attributes)) { + $attributes = array_intersect($this->attributes, $attributes); + } else { + $attributes = $this->attributes; + } + foreach ($attributes as $attribute) { + $skip = $this->skipOnError && $object->hasErrors($attribute) + || $this->skipOnEmpty && $this->isEmpty($object->$attribute); + if (!$skip) { + $this->validateAttribute($object, $attribute); + } + } + } - /** - * Validates the specified object. - * @param \yii\base\Model $object the data object being validated - * @param array|null $attributes the list of attributes to be validated. - * Note that if an attribute is not associated with the validator, - * it will be ignored. - * If this parameter is null, every attribute listed in [[attributes]] will be validated. - */ - public function validateAttributes($object, $attributes = null) - { - if (is_array($attributes)) { - $attributes = array_intersect($this->attributes, $attributes); - } else { - $attributes = $this->attributes; - } - foreach ($attributes as $attribute) { - $skip = $this->skipOnError && $object->hasErrors($attribute) - || $this->skipOnEmpty && $this->isEmpty($object->$attribute); - if (!$skip) { - $this->validateAttribute($object, $attribute); - } - } - } + /** + * Validates a single attribute. + * Child classes must implement this method to provide the actual validation logic. + * @param \yii\base\Model $object the data object to be validated + * @param string $attribute the name of the attribute to be validated. + */ + public function validateAttribute($object, $attribute) + { + $result = $this->validateValue($object->$attribute); + if (!empty($result)) { + $this->addError($object, $attribute, $result[0], $result[1]); + } + } - /** - * Validates a single attribute. - * Child classes must implement this method to provide the actual validation logic. - * @param \yii\base\Model $object the data object to be validated - * @param string $attribute the name of the attribute to be validated. - */ - public function validateAttribute($object, $attribute) - { - $result = $this->validateValue($object->$attribute); - if (!empty($result)) { - $this->addError($object, $attribute, $result[0], $result[1]); - } - } + /** + * Validates a given value. + * You may use this method to validate a value out of the context of a data model. + * @param mixed $value the data value to be validated. + * @param string $error the error message to be returned, if the validation fails. + * @return boolean whether the data is valid. + */ + public function validate($value, &$error = null) + { + $result = $this->validateValue($value); + if (empty($result)) { + return true; + } else { + list($message, $params) = $result; + $params['attribute'] = Yii::t('yii', 'the input value'); + $params['value'] = is_array($value) ? 'array()' : $value; + $error = Yii::$app->getI18n()->format($message, $params, Yii::$app->language); - /** - * Validates a given value. - * You may use this method to validate a value out of the context of a data model. - * @param mixed $value the data value to be validated. - * @param string $error the error message to be returned, if the validation fails. - * @return boolean whether the data is valid. - */ - public function validate($value, &$error = null) - { - $result = $this->validateValue($value); - if (empty($result)) { - return true; - } else { - list($message, $params) = $result; - $params['attribute'] = Yii::t('yii', 'the input value'); - $params['value'] = is_array($value) ? 'array()' : $value; - $error = Yii::$app->getI18n()->format($message, $params, Yii::$app->language); - return false; - } - } + return false; + } + } - /** - * Validates a value. - * A validator class can implement this method to support data validation out of the context of a data model. - * @param mixed $value the data value to be validated. - * @return array|null the error message and the parameters to be inserted into the error message. - * Null should be returned if the data is valid. - * @throws NotSupportedException if the validator does not supporting data validation without a model - */ - protected function validateValue($value) - { - throw new NotSupportedException(get_class($this) . ' does not support validateValue().'); - } + /** + * Validates a value. + * A validator class can implement this method to support data validation out of the context of a data model. + * @param mixed $value the data value to be validated. + * @return array|null the error message and the parameters to be inserted into the error message. + * Null should be returned if the data is valid. + * @throws NotSupportedException if the validator does not supporting data validation without a model + */ + protected function validateValue($value) + { + throw new NotSupportedException(get_class($this) . ' does not support validateValue().'); + } - /** - * Returns the JavaScript needed for performing client-side validation. - * - * You may override this method to return the JavaScript validation code if - * the validator can support client-side validation. - * - * The following JavaScript variables are predefined and can be used in the validation code: - * - * - `attribute`: the name of the attribute being validated. - * - `value`: the value being validated. - * - `messages`: an array used to hold the validation error messages for the attribute. - * - * @param \yii\base\Model $object the data object being validated - * @param string $attribute the name of the attribute to be validated. - * @param \yii\web\View $view the view object that is going to be used to render views or view files - * containing a model form with this validator applied. - * @return string the client-side validation script. Null if the validator does not support - * client-side validation. - * @see \yii\widgets\ActiveForm::enableClientValidation - */ - public function clientValidateAttribute($object, $attribute, $view) - { - return null; - } + /** + * Returns the JavaScript needed for performing client-side validation. + * + * You may override this method to return the JavaScript validation code if + * the validator can support client-side validation. + * + * The following JavaScript variables are predefined and can be used in the validation code: + * + * - `attribute`: the name of the attribute being validated. + * - `value`: the value being validated. + * - `messages`: an array used to hold the validation error messages for the attribute. + * + * @param \yii\base\Model $object the data object being validated + * @param string $attribute the name of the attribute to be validated. + * @param \yii\web\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. + * @return string the client-side validation script. Null if the validator does not support + * client-side validation. + * @see \yii\widgets\ActiveForm::enableClientValidation + */ + public function clientValidateAttribute($object, $attribute, $view) + { + return null; + } - /** - * Returns a value indicating whether the validator is active for the given scenario and attribute. - * - * A validator is active if - * - * - the validator's `on` property is empty, or - * - the validator's `on` property contains the specified scenario - * - * @param string $scenario scenario name - * @return boolean whether the validator applies to the specified scenario. - */ - public function isActive($scenario) - { - return !in_array($scenario, $this->except, true) && (empty($this->on) || in_array($scenario, $this->on, true)); - } + /** + * Returns a value indicating whether the validator is active for the given scenario and attribute. + * + * A validator is active if + * + * - the validator's `on` property is empty, or + * - the validator's `on` property contains the specified scenario + * + * @param string $scenario scenario name + * @return boolean whether the validator applies to the specified scenario. + */ + public function isActive($scenario) + { + return !in_array($scenario, $this->except, true) && (empty($this->on) || in_array($scenario, $this->on, true)); + } - /** - * Adds an error about the specified attribute to the model object. - * This is a helper method that performs message selection and internationalization. - * @param \yii\base\Model $object the data object being validated - * @param string $attribute the attribute being validated - * @param string $message the error message - * @param array $params values for the placeholders in the error message - */ - public function addError($object, $attribute, $message, $params = []) - { - $value = $object->$attribute; - $params['attribute'] = $object->getAttributeLabel($attribute); - $params['value'] = is_array($value) ? 'array()' : $value; - $object->addError($attribute, Yii::$app->getI18n()->format($message, $params, Yii::$app->language)); - } + /** + * Adds an error about the specified attribute to the model object. + * This is a helper method that performs message selection and internationalization. + * @param \yii\base\Model $object the data object being validated + * @param string $attribute the attribute being validated + * @param string $message the error message + * @param array $params values for the placeholders in the error message + */ + public function addError($object, $attribute, $message, $params = []) + { + $value = $object->$attribute; + $params['attribute'] = $object->getAttributeLabel($attribute); + $params['value'] = is_array($value) ? 'array()' : $value; + $object->addError($attribute, Yii::$app->getI18n()->format($message, $params, Yii::$app->language)); + } - /** - * Checks if the given value is empty. - * A value is considered empty if it is null, an empty array, or the trimmed result is an empty string. - * Note that this method is different from PHP empty(). It will return false when the value is 0. - * @param mixed $value the value to be checked - * @param boolean $trim whether to perform trimming before checking if the string is empty. Defaults to false. - * @return boolean whether the value is empty - */ - public function isEmpty($value, $trim = false) - { - return $value === null || $value === [] || $value === '' - || $trim && is_scalar($value) && trim($value) === ''; - } + /** + * Checks if the given value is empty. + * A value is considered empty if it is null, an empty array, or the trimmed result is an empty string. + * Note that this method is different from PHP empty(). It will return false when the value is 0. + * @param mixed $value the value to be checked + * @param boolean $trim whether to perform trimming before checking if the string is empty. Defaults to false. + * @return boolean whether the value is empty + */ + public function isEmpty($value, $trim = false) + { + return $value === null || $value === [] || $value === '' + || $trim && is_scalar($value) && trim($value) === ''; + } } diff --git a/framework/views/errorHandler/callStackItem.php b/framework/views/errorHandler/callStackItem.php index 566e1d317dc..eda747c82aa 100644 --- a/framework/views/errorHandler/callStackItem.php +++ b/framework/views/errorHandler/callStackItem.php @@ -12,34 +12,34 @@ */ ?>
  • -
    -
    - . - htmlEncode($file); ?> - - - - addTypeLinks($class) . '::'; ?>addTypeLinks($method . '()') ?> - - - - -
    -
    - -
    -
    -
    -
    - -
    htmlEncode($lines[$i]);
    -					}
    -				?>
    -
    -
    - + data-line=""> +
    +
    + . + htmlEncode($file); ?> + + + + addTypeLinks($class) . '::'; ?>addTypeLinks($method . '()') ?> + + + + +
    +
    + +
    +
    +
    +
    + +
    htmlEncode($lines[$i]);
    +                    }
    +                ?>
    +
    +
    +
  • diff --git a/framework/views/errorHandler/error.php b/framework/views/errorHandler/error.php index 066d7e4ffa1..a72969ee08c 100644 --- a/framework/views/errorHandler/error.php +++ b/framework/views/errorHandler/error.php @@ -4,83 +4,83 @@ * @var \yii\base\ErrorHandler $handler */ if ($exception instanceof \yii\web\HttpException) { - $code = $exception->statusCode; + $code = $exception->statusCode; } else { - $code = $exception->getCode(); + $code = $exception->getCode(); } if ($exception instanceof \yii\base\Exception) { - $name = $exception->getName(); + $name = $exception->getName(); } else { - $name = 'Error'; + $name = 'Error'; } if ($code) { - $name .= " (#$code)"; + $name .= " (#$code)"; } if ($exception instanceof \yii\base\UserException) { - $message = $exception->getMessage(); + $message = $exception->getMessage(); } else { - $message = 'An internal server error occurred.'; + $message = 'An internal server error occurred.'; } ?> beginPage(); ?> - - <?= $handler->htmlEncode($name) ?> + + <?= $handler->htmlEncode($name) ?> - + .version { + color: gray; + font-size: 8pt; + border-top: 1px solid #aaa; + padding-top: 1em; + margin-bottom: 1em; + } + -

    htmlEncode($name) ?>

    -

    htmlEncode($message)) ?>

    -

    - The above error occurred while the Web server was processing your request. -

    -

    - Please contact us if you think this is a server error. Thank you. -

    -
    - -
    - endBody(); // to allow injecting code into body (mostly by Yii Debug Toolbar) ?> +

    htmlEncode($name) ?>

    +

    htmlEncode($message)) ?>

    +

    + The above error occurred while the Web server was processing your request. +

    +

    + Please contact us if you think this is a server error. Thank you. +

    +
    + +
    + endBody(); // to allow injecting code into body (mostly by Yii Debug Toolbar) ?> endPage(); ?> diff --git a/framework/views/errorHandler/exception.php b/framework/views/errorHandler/exception.php index f9ea9edfbe1..e69de29bb2d 100644 --- a/framework/views/errorHandler/exception.php +++ b/framework/views/errorHandler/exception.php @@ -1,482 +0,0 @@ - -beginPage(); ?> - - - - - - - <?php - if ($exception instanceof \yii\web\HttpException) { - echo (int) $exception->statusCode . ' ' . $handler->htmlEncode($exception->getName()); - } elseif ($exception instanceof \yii\base\Exception) { - echo $handler->htmlEncode($exception->getName() . ' – ' . get_class($exception)); - } else { - echo $handler->htmlEncode(get_class($exception)); - } - ?> - - - - - -
    - - Error -

    - htmlEncode($exception->getName()) ?> - – addTypeLinks(get_class($exception)) ?> -

    - - Exception -

    ' . $handler->createHttpStatusLink($exception->statusCode, $handler->htmlEncode($exception->getName())) . ''; - echo ' – ' . $handler->addTypeLinks(get_class($exception)); - } elseif ($exception instanceof \yii\base\Exception) { - echo '' . $handler->htmlEncode($exception->getName()) . ''; - echo ' – ' . $handler->addTypeLinks(get_class($exception)); - } else { - echo '' . $handler->htmlEncode(get_class($exception)) . ''; - } - ?>

    - -

    htmlEncode($exception->getMessage())) ?>

    - - renderPreviousExceptions($exception) ?> -
    - -
    -
      - renderCallStackItem($exception->getFile(), $exception->getLine(), null, null, 1) ?> - getTrace(), $length = count($trace); $i < $length; ++$i): ?> - renderCallStackItem(@$trace[$i]['file'] ?: null, @$trace[$i]['line'] ?: null, - @$trace[$i]['class'] ?: null, @$trace[$i]['function'] ?: null, $i + 2) ?> - -
    -
    - -
    -
    - renderRequest() ?> -
    -
    - - - - - - - - - endBody(); // to allow injecting code into body (mostly by Yii Debug Toolbar) ?> - - - -endPage(); ?> diff --git a/framework/views/errorHandler/previousException.php b/framework/views/errorHandler/previousException.php index 766c0a786fd..34749c7510d 100644 --- a/framework/views/errorHandler/previousException.php +++ b/framework/views/errorHandler/previousException.php @@ -5,17 +5,17 @@ */ ?> diff --git a/framework/views/messageConfig.php b/framework/views/messageConfig.php index f926c829f60..6b4e8c9b7ec 100644 --- a/framework/views/messageConfig.php +++ b/framework/views/messageConfig.php @@ -1,52 +1,52 @@ __DIR__, - // string, required, root directory containing message translations. - 'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages', - // array, required, list of language codes that the extracted messages - // should be translated to. For example, ['zh-CN', 'de']. - 'languages' => ['de'], - // string, the name of the function for translating messages. - // Defaults to 'Yii::t'. This is used as a mark to find the messages to be - // translated. You may use a string for single function name or an array for - // multiple function names. - 'translator' => 'Yii::t', - // boolean, whether to sort messages by keys when merging new messages - // with the existing ones. Defaults to false, which means the new (untranslated) - // messages will be separated from the old (translated) ones. - 'sort' => false, - // boolean, whether the message file should be overwritten with the merged messages - 'overwrite' => true, - // boolean, whether to remove messages that no longer appear in the source code. - // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. - 'removeUnused' => false, - // array, list of patterns that specify which files/directories should NOT be processed. - // If empty or not set, all files/directories will be processed. - // A path matches a pattern if it contains the pattern string at its end. For example, - // '/a/b' will match all files and directories ending with '/a/b'; - // the '*.svn' will match all files and directories whose name ends with '.svn'. - // and the '.svn' will match all files and directories named exactly '.svn'. - // Note, the '/' characters in a pattern matches both '/' and '\'. - // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. - 'only' => ['*.php'], - // array, list of patterns that specify which files (not directories) should be processed. - // If empty or not set, all files will be processed. - // Please refer to "except" for details about the patterns. - // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. - 'except' => [ - '.svn', - '.git', - '.gitignore', - '.gitkeep', - '.hgignore', - '.hgkeep', - '/messages', - ], - // Generated file format. Can be either "php", "po" or "db". - 'format' => 'php', - // When format is "db", you may specify the following two options - //'db' => 'db', - //'sourceMessageTable' => '{{%source_message}}', + // string, required, root directory of all source files + 'sourcePath' => __DIR__, + // string, required, root directory containing message translations. + 'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages', + // array, required, list of language codes that the extracted messages + // should be translated to. For example, ['zh-CN', 'de']. + 'languages' => ['de'], + // string, the name of the function for translating messages. + // Defaults to 'Yii::t'. This is used as a mark to find the messages to be + // translated. You may use a string for single function name or an array for + // multiple function names. + 'translator' => 'Yii::t', + // boolean, whether to sort messages by keys when merging new messages + // with the existing ones. Defaults to false, which means the new (untranslated) + // messages will be separated from the old (translated) ones. + 'sort' => false, + // boolean, whether the message file should be overwritten with the merged messages + 'overwrite' => true, + // boolean, whether to remove messages that no longer appear in the source code. + // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. + 'removeUnused' => false, + // array, list of patterns that specify which files/directories should NOT be processed. + // If empty or not set, all files/directories will be processed. + // A path matches a pattern if it contains the pattern string at its end. For example, + // '/a/b' will match all files and directories ending with '/a/b'; + // the '*.svn' will match all files and directories whose name ends with '.svn'. + // and the '.svn' will match all files and directories named exactly '.svn'. + // Note, the '/' characters in a pattern matches both '/' and '\'. + // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. + 'only' => ['*.php'], + // array, list of patterns that specify which files (not directories) should be processed. + // If empty or not set, all files will be processed. + // Please refer to "except" for details about the patterns. + // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. + 'except' => [ + '.svn', + '.git', + '.gitignore', + '.gitkeep', + '.hgignore', + '.hgkeep', + '/messages', + ], + // Generated file format. Can be either "php", "po" or "db". + 'format' => 'php', + // When format is "db", you may specify the following two options + //'db' => 'db', + //'sourceMessageTable' => '{{%source_message}}', ]; diff --git a/framework/views/migration.php b/framework/views/migration.php index f6add4d151d..c0e6a9810e9 100644 --- a/framework/views/migration.php +++ b/framework/views/migration.php @@ -8,18 +8,17 @@ echo " -use yii\db\Schema; - class extends \yii\db\Migration { - public function up() - { + public function up() + { + + } - } + public function down() + { + echo " cannot be reverted.\n"; - public function down() - { - echo " cannot be reverted.\n"; - return false; - } + return false; + } } diff --git a/framework/web/AccessControl.php b/framework/web/AccessControl.php index c4b38e4dece..89441a7e37d 100644 --- a/framework/web/AccessControl.php +++ b/framework/web/AccessControl.php @@ -53,91 +53,93 @@ */ class AccessControl extends ActionFilter { - /** - * @var callable a callback that will be called if the access should be denied - * to the current user. If not set, [[denyAccess()]] will be called. - * - * The signature of the callback should be as follows: - * - * ~~~ - * function ($rule, $action) - * ~~~ - * - * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. - */ - public $denyCallback; - /** - * @var array the default configuration of access rules. Individual rule configurations - * specified via [[rules]] will take precedence when the same property of the rule is configured. - */ - public $ruleConfig = ['class' => 'yii\web\AccessRule']; - /** - * @var array a list of access rule objects or configuration arrays for creating the rule objects. - * If a rule is specified via a configuration array, it will be merged with [[ruleConfig]] first - * before it is used for creating the rule object. - * @see ruleConfig - */ - public $rules = []; + /** + * @var callable a callback that will be called if the access should be denied + * to the current user. If not set, [[denyAccess()]] will be called. + * + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + */ + public $denyCallback; + /** + * @var array the default configuration of access rules. Individual rule configurations + * specified via [[rules]] will take precedence when the same property of the rule is configured. + */ + public $ruleConfig = ['class' => 'yii\web\AccessRule']; + /** + * @var array a list of access rule objects or configuration arrays for creating the rule objects. + * If a rule is specified via a configuration array, it will be merged with [[ruleConfig]] first + * before it is used for creating the rule object. + * @see ruleConfig + */ + public $rules = []; - /** - * Initializes the [[rules]] array by instantiating rule objects from configurations. - */ - public function init() - { - parent::init(); - foreach ($this->rules as $i => $rule) { - if (is_array($rule)) { - $this->rules[$i] = Yii::createObject(array_merge($this->ruleConfig, $rule)); - } - } - } + /** + * Initializes the [[rules]] array by instantiating rule objects from configurations. + */ + public function init() + { + parent::init(); + foreach ($this->rules as $i => $rule) { + if (is_array($rule)) { + $this->rules[$i] = Yii::createObject(array_merge($this->ruleConfig, $rule)); + } + } + } - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $user = Yii::$app->getUser(); - $request = Yii::$app->getRequest(); - /** @var AccessRule $rule */ - foreach ($this->rules as $rule) { - if ($allow = $rule->allows($action, $user, $request)) { - return true; - } elseif ($allow === false) { - if (isset($rule->denyCallback)) { - call_user_func($rule->denyCallback, $rule, $action); - } elseif (isset($this->denyCallback)) { - call_user_func($this->denyCallback, $rule, $action); - } else { - $this->denyAccess($user); - } - return false; - } - } - if (isset($this->denyCallback)) { - call_user_func($this->denyCallback, $rule, $action); - } else { - $this->denyAccess($user); - } - return false; - } + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $user = Yii::$app->getUser(); + $request = Yii::$app->getRequest(); + /** @var AccessRule $rule */ + foreach ($this->rules as $rule) { + if ($allow = $rule->allows($action, $user, $request)) { + return true; + } elseif ($allow === false) { + if (isset($rule->denyCallback)) { + call_user_func($rule->denyCallback, $rule, $action); + } elseif (isset($this->denyCallback)) { + call_user_func($this->denyCallback, $rule, $action); + } else { + $this->denyAccess($user); + } - /** - * Denies the access of the user. - * The default implementation will redirect the user to the login page if he is a guest; - * if the user is already logged, a 403 HTTP exception will be thrown. - * @param User $user the current user - * @throws ForbiddenHttpException if the user is already logged in. - */ - protected function denyAccess($user) - { - if ($user->getIsGuest()) { - $user->loginRequired(); - } else { - throw new ForbiddenHttpException(Yii::t('yii', 'You are not allowed to perform this action.')); - } - } + return false; + } + } + if (isset($this->denyCallback)) { + call_user_func($this->denyCallback, $rule, $action); + } else { + $this->denyAccess($user); + } + + return false; + } + + /** + * Denies the access of the user. + * The default implementation will redirect the user to the login page if he is a guest; + * if the user is already logged, a 403 HTTP exception will be thrown. + * @param User $user the current user + * @throws ForbiddenHttpException if the user is already logged in. + */ + protected function denyAccess($user) + { + if ($user->getIsGuest()) { + $user->loginRequired(); + } else { + throw new ForbiddenHttpException(Yii::t('yii', 'You are not allowed to perform this action.')); + } + } } diff --git a/framework/web/AccessRule.php b/framework/web/AccessRule.php index eb1011646f2..18389adc254 100644 --- a/framework/web/AccessRule.php +++ b/framework/web/AccessRule.php @@ -18,169 +18,170 @@ */ class AccessRule extends Component { - /** - * @var boolean whether this is an 'allow' rule or 'deny' rule. - */ - public $allow; - /** - * @var array list of action IDs that this rule applies to. The comparison is case-sensitive. - * If not set or empty, it means this rule applies to all actions. - */ - public $actions; - /** - * @var array list of controller IDs that this rule applies to. The comparison is case-sensitive. - * If not set or empty, it means this rule applies to all controllers. - */ - public $controllers; - /** - * @var array list of roles that this rule applies to. Two special roles are recognized, and - * they are checked via [[User::isGuest]]: - * - * - `?`: matches a guest user (not authenticated yet) - * - `@`: matches an authenticated user - * - * Using additional role names requires RBAC (Role-Based Access Control), and - * [[User::checkAccess()]] will be called. - * - * If this property is not set or empty, it means this rule applies to all roles. - */ - public $roles; - /** - * @var array list of user IP addresses that this rule applies to. An IP address - * can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix. - * For example, '192.168.*' matches all IP addresses in the segment '192.168.'. - * If not set or empty, it means this rule applies to all IP addresses. - * @see Request::userIP - */ - public $ips; - /** - * @var array list of request methods (e.g. `GET`, `POST`) that this rule applies to. - * The request methods must be specified in uppercase. - * If not set or empty, it means this rule applies to all request methods. - * @see Request::requestMethod - */ - public $verbs; - /** - * @var callable a callback that will be called to determine if the rule should be applied. - * The signature of the callback should be as follows: - * - * ~~~ - * function ($rule, $action) - * ~~~ - * - * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. - * The callback should return a boolean value indicating whether this rule should be applied. - */ - public $matchCallback; - /** - * @var callable a callback that will be called if this rule determines the access to - * the current action should be denied. If not set, the behavior will be determined by - * [[AccessControl]]. - * - * The signature of the callback should be as follows: - * - * ~~~ - * function ($rule, $action) - * ~~~ - * - * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. - */ - public $denyCallback; + /** + * @var boolean whether this is an 'allow' rule or 'deny' rule. + */ + public $allow; + /** + * @var array list of action IDs that this rule applies to. The comparison is case-sensitive. + * If not set or empty, it means this rule applies to all actions. + */ + public $actions; + /** + * @var array list of controller IDs that this rule applies to. The comparison is case-sensitive. + * If not set or empty, it means this rule applies to all controllers. + */ + public $controllers; + /** + * @var array list of roles that this rule applies to. Two special roles are recognized, and + * they are checked via [[User::isGuest]]: + * + * - `?`: matches a guest user (not authenticated yet) + * - `@`: matches an authenticated user + * + * Using additional role names requires RBAC (Role-Based Access Control), and + * [[User::checkAccess()]] will be called. + * + * If this property is not set or empty, it means this rule applies to all roles. + */ + public $roles; + /** + * @var array list of user IP addresses that this rule applies to. An IP address + * can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix. + * For example, '192.168.*' matches all IP addresses in the segment '192.168.'. + * If not set or empty, it means this rule applies to all IP addresses. + * @see Request::userIP + */ + public $ips; + /** + * @var array list of request methods (e.g. `GET`, `POST`) that this rule applies to. + * The request methods must be specified in uppercase. + * If not set or empty, it means this rule applies to all request methods. + * @see Request::requestMethod + */ + public $verbs; + /** + * @var callable a callback that will be called to determine if the rule should be applied. + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + * The callback should return a boolean value indicating whether this rule should be applied. + */ + public $matchCallback; + /** + * @var callable a callback that will be called if this rule determines the access to + * the current action should be denied. If not set, the behavior will be determined by + * [[AccessControl]]. + * + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + */ + public $denyCallback; + /** + * Checks whether the Web user is allowed to perform the specified action. + * @param Action $action the action to be performed + * @param User $user the user object + * @param Request $request + * @return boolean|null true if the user is allowed, false if the user is denied, null if the rule does not apply to the user + */ + public function allows($action, $user, $request) + { + if ($this->matchAction($action) + && $this->matchRole($user) + && $this->matchIP($request->getUserIP()) + && $this->matchVerb($request->getMethod()) + && $this->matchController($action->controller) + && $this->matchCustom($action) + ) { + return $this->allow ? true : false; + } else { + return null; + } + } - /** - * Checks whether the Web user is allowed to perform the specified action. - * @param Action $action the action to be performed - * @param User $user the user object - * @param Request $request - * @return boolean|null true if the user is allowed, false if the user is denied, null if the rule does not apply to the user - */ - public function allows($action, $user, $request) - { - if ($this->matchAction($action) - && $this->matchRole($user) - && $this->matchIP($request->getUserIP()) - && $this->matchVerb($request->getMethod()) - && $this->matchController($action->controller) - && $this->matchCustom($action) - ) { - return $this->allow ? true : false; - } else { - return null; - } - } + /** + * @param Action $action the action + * @return boolean whether the rule applies to the action + */ + protected function matchAction($action) + { + return empty($this->actions) || in_array($action->id, $this->actions, true); + } - /** - * @param Action $action the action - * @return boolean whether the rule applies to the action - */ - protected function matchAction($action) - { - return empty($this->actions) || in_array($action->id, $this->actions, true); - } + /** + * @param Controller $controller the controller + * @return boolean whether the rule applies to the controller + */ + protected function matchController($controller) + { + return empty($this->controllers) || in_array($controller->uniqueId, $this->controllers, true); + } - /** - * @param Controller $controller the controller - * @return boolean whether the rule applies to the controller - */ - protected function matchController($controller) - { - return empty($this->controllers) || in_array($controller->uniqueId, $this->controllers, true); - } + /** + * @param User $user the user object + * @return boolean whether the rule applies to the role + */ + protected function matchRole($user) + { + if (empty($this->roles)) { + return true; + } + foreach ($this->roles as $role) { + if ($role === '?' && $user->getIsGuest()) { + return true; + } elseif ($role === '@' && !$user->getIsGuest()) { + return true; + } elseif ($user->checkAccess($role)) { + return true; + } + } - /** - * @param User $user the user object - * @return boolean whether the rule applies to the role - */ - protected function matchRole($user) - { - if (empty($this->roles)) { - return true; - } - foreach ($this->roles as $role) { - if ($role === '?' && $user->getIsGuest()) { - return true; - } elseif ($role === '@' && !$user->getIsGuest()) { - return true; - } elseif ($user->checkAccess($role)) { - return true; - } - } - return false; - } + return false; + } - /** - * @param string $ip the IP address - * @return boolean whether the rule applies to the IP address - */ - protected function matchIP($ip) - { - if (empty($this->ips)) { - return true; - } - foreach ($this->ips as $rule) { - if ($rule === '*' || $rule === $ip || (($pos = strpos($rule, '*')) !== false && !strncmp($ip, $rule, $pos))) { - return true; - } - } - return false; - } + /** + * @param string $ip the IP address + * @return boolean whether the rule applies to the IP address + */ + protected function matchIP($ip) + { + if (empty($this->ips)) { + return true; + } + foreach ($this->ips as $rule) { + if ($rule === '*' || $rule === $ip || (($pos = strpos($rule, '*')) !== false && !strncmp($ip, $rule, $pos))) { + return true; + } + } - /** - * @param string $verb the request method - * @return boolean whether the rule applies to the request - */ - protected function matchVerb($verb) - { - return empty($this->verbs) || in_array($verb, $this->verbs, true); - } + return false; + } - /** - * @param Action $action the action to be performed - * @return boolean whether the rule should be applied - */ - protected function matchCustom($action) - { - return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action); - } + /** + * @param string $verb the request method + * @return boolean whether the rule applies to the request + */ + protected function matchVerb($verb) + { + return empty($this->verbs) || in_array($verb, $this->verbs, true); + } + + /** + * @param Action $action the action to be performed + * @return boolean whether the rule should be applied + */ + protected function matchCustom($action) + { + return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action); + } } diff --git a/framework/web/Application.php b/framework/web/Application.php index f1fcff06f61..f991ec3582e 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -25,162 +25,162 @@ */ class Application extends \yii\base\Application { - /** - * @var string the default route of this application. Defaults to 'site'. - */ - public $defaultRoute = 'site'; - /** - * @var array the configuration specifying a controller action which should handle - * all user requests. This is mainly used when the application is in maintenance mode - * and needs to handle all incoming requests via a single action. - * The configuration is an array whose first element specifies the route of the action. - * The rest of the array elements (key-value pairs) specify the parameters to be bound - * to the action. For example, - * - * ~~~ - * [ - * 'offline/notice', - * 'param1' => 'value1', - * 'param2' => 'value2', - * ] - * ~~~ - * - * Defaults to null, meaning catch-all is not used. - */ - public $catchAll; - /** - * @var Controller the currently active controller instance - */ - public $controller; + /** + * @var string the default route of this application. Defaults to 'site'. + */ + public $defaultRoute = 'site'; + /** + * @var array the configuration specifying a controller action which should handle + * all user requests. This is mainly used when the application is in maintenance mode + * and needs to handle all incoming requests via a single action. + * The configuration is an array whose first element specifies the route of the action. + * The rest of the array elements (key-value pairs) specify the parameters to be bound + * to the action. For example, + * + * ~~~ + * [ + * 'offline/notice', + * 'param1' => 'value1', + * 'param2' => 'value2', + * ] + * ~~~ + * + * Defaults to null, meaning catch-all is not used. + */ + public $catchAll; + /** + * @var Controller the currently active controller instance + */ + public $controller; + /** + * @inheritdoc + */ + public function preloadComponents() + { + parent::preloadComponents(); + $request = $this->getRequest(); + Yii::setAlias('@webroot', dirname($request->getScriptFile())); + Yii::setAlias('@web', $request->getBaseUrl()); + } - /** - * @inheritdoc - */ - public function preloadComponents() - { - parent::preloadComponents(); - $request = $this->getRequest(); - Yii::setAlias('@webroot', dirname($request->getScriptFile())); - Yii::setAlias('@web', $request->getBaseUrl()); - } + /** + * Handles the specified request. + * @param Request $request the request to be handled + * @return Response the resulting response + * @throws NotFoundHttpException if the requested route is invalid + */ + public function handleRequest($request) + { + if (empty($this->catchAll)) { + list ($route, $params) = $request->resolve(); + } else { + $route = $this->catchAll[0]; + $params = array_splice($this->catchAll, 1); + } + try { + Yii::trace("Route requested: '$route'", __METHOD__); + $this->requestedRoute = $route; + $result = $this->runAction($route, $params); + if ($result instanceof Response) { + return $result; + } else { + $response = $this->getResponse(); + if ($result !== null) { + $response->data = $result; + } - /** - * Handles the specified request. - * @param Request $request the request to be handled - * @return Response the resulting response - * @throws NotFoundHttpException if the requested route is invalid - */ - public function handleRequest($request) - { - if (empty($this->catchAll)) { - list ($route, $params) = $request->resolve(); - } else { - $route = $this->catchAll[0]; - $params = array_splice($this->catchAll, 1); - } - try { - Yii::trace("Route requested: '$route'", __METHOD__); - $this->requestedRoute = $route; - $result = $this->runAction($route, $params); - if ($result instanceof Response) { - return $result; - } else { - $response = $this->getResponse(); - if ($result !== null) { - $response->data = $result; - } - return $response; - } - } catch (InvalidRouteException $e) { - throw new NotFoundHttpException($e->getMessage(), $e->getCode(), $e); - } - } + return $response; + } + } catch (InvalidRouteException $e) { + throw new NotFoundHttpException($e->getMessage(), $e->getCode(), $e); + } + } - private $_homeUrl; + private $_homeUrl; - /** - * @return string the homepage URL - */ - public function getHomeUrl() - { - if ($this->_homeUrl === null) { - if ($this->getUrlManager()->showScriptName) { - return $this->getRequest()->getScriptUrl(); - } else { - return $this->getRequest()->getBaseUrl() . '/'; - } - } else { - return $this->_homeUrl; - } - } + /** + * @return string the homepage URL + */ + public function getHomeUrl() + { + if ($this->_homeUrl === null) { + if ($this->getUrlManager()->showScriptName) { + return $this->getRequest()->getScriptUrl(); + } else { + return $this->getRequest()->getBaseUrl() . '/'; + } + } else { + return $this->_homeUrl; + } + } - /** - * @param string $value the homepage URL - */ - public function setHomeUrl($value) - { - $this->_homeUrl = $value; - } + /** + * @param string $value the homepage URL + */ + public function setHomeUrl($value) + { + $this->_homeUrl = $value; + } - /** - * Returns the request component. - * @return Request the request component - */ - public function getRequest() - { - return $this->getComponent('request'); - } + /** + * Returns the request component. + * @return Request the request component + */ + public function getRequest() + { + return $this->getComponent('request'); + } - /** - * Returns the response component. - * @return Response the response component - */ - public function getResponse() - { - return $this->getComponent('response'); - } + /** + * Returns the response component. + * @return Response the response component + */ + public function getResponse() + { + return $this->getComponent('response'); + } - /** - * Returns the session component. - * @return Session the session component - */ - public function getSession() - { - return $this->getComponent('session'); - } + /** + * Returns the session component. + * @return Session the session component + */ + public function getSession() + { + return $this->getComponent('session'); + } - /** - * Returns the user component. - * @return User the user component - */ - public function getUser() - { - return $this->getComponent('user'); - } + /** + * Returns the user component. + * @return User the user component + */ + public function getUser() + { + return $this->getComponent('user'); + } - /** - * Returns the asset manager. - * @return AssetManager the asset manager component - */ - public function getAssetManager() - { - return $this->getComponent('assetManager'); - } + /** + * Returns the asset manager. + * @return AssetManager the asset manager component + */ + public function getAssetManager() + { + return $this->getComponent('assetManager'); + } - /** - * Registers the core application components. - * @see setComponents - */ - public function registerCoreComponents() - { - parent::registerCoreComponents(); - $this->setComponents([ - 'request' => ['class' => 'yii\web\Request'], - 'response' => ['class' => 'yii\web\Response'], - 'session' => ['class' => 'yii\web\Session'], - 'user' => ['class' => 'yii\web\User'], - 'assetManager' => ['class' => 'yii\web\AssetManager'], - ]); - } + /** + * Registers the core application components. + * @see setComponents + */ + public function registerCoreComponents() + { + parent::registerCoreComponents(); + $this->setComponents([ + 'request' => ['class' => 'yii\web\Request'], + 'response' => ['class' => 'yii\web\Response'], + 'session' => ['class' => 'yii\web\Session'], + 'user' => ['class' => 'yii\web\User'], + 'assetManager' => ['class' => 'yii\web\AssetManager'], + ]); + } } diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 8fcdf2cfcca..3321bf511e2 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -25,168 +25,168 @@ */ class AssetBundle extends Object { - /** - * @var string the root directory of the source asset files. A source asset file - * is a file that is part of your source code repository of your Web application. - * - * You must set this property if the directory containing the source asset files - * is not Web accessible (this is usually the case for extensions). - * - * By setting this property, the asset manager will publish the source asset files - * to a Web-accessible directory [[basePath]]. - * - * You can use either a directory or an alias of the directory. - */ - public $sourcePath; - /** - * @var string the Web-accessible directory that contains the asset files in this bundle. - * - * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] - * when it publishes the asset files from [[sourcePath]]. - * - * If the bundle contains any assets that are specified in terms of relative file path, - * then this property must be set either manually or automatically (by [[AssetManager]] via - * asset publishing). - * - * You can use either a directory or an alias of the directory. - */ - public $basePath; - /** - * @var string the base URL that will be prefixed to the asset files for them to - * be accessed via Web server. - * - * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] - * when it publishes the asset files from [[sourcePath]]. - * - * If the bundle contains any assets that are specified in terms of relative file path, - * then this property must be set either manually or automatically (by asset manager via - * asset publishing). - * - * You can use either a URL or an alias of the URL. - */ - public $baseUrl; - /** - * @var array list of bundle class names that this bundle depends on. - * - * For example: - * - * ```php - * public $depends = [ - * 'yii\web\YiiAsset', - * 'yii\bootstrap\BootstrapAsset', - * ]; - * ``` - */ - public $depends = []; - /** - * @var array list of JavaScript files that this bundle contains. Each JavaScript file can - * be either a file path (without leading slash) relative to [[basePath]] or a URL representing - * an external JavaScript file. - * - * Note that only forward slash "/" can be used as directory separator. - */ - public $js = []; - /** - * @var array list of CSS files that this bundle contains. Each CSS file can - * be either a file path (without leading slash) relative to [[basePath]] or a URL representing - * an external CSS file. - * - * Note that only forward slash "/" can be used as directory separator. - */ - public $css = []; - /** - * @var array the options that will be passed to [[\yii\web\View::registerJsFile()]] - * when registering the JS files in this bundle. - */ - public $jsOptions = []; - /** - * @var array the options that will be passed to [[\yii\web\View::registerCssFile()]] - * when registering the CSS files in this bundle. - */ - public $cssOptions = []; - /** - * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle - * is being published. - */ - public $publishOptions = []; + /** + * @var string the root directory of the source asset files. A source asset file + * is a file that is part of your source code repository of your Web application. + * + * You must set this property if the directory containing the source asset files + * is not Web accessible (this is usually the case for extensions). + * + * By setting this property, the asset manager will publish the source asset files + * to a Web-accessible directory [[basePath]]. + * + * You can use either a directory or an alias of the directory. + */ + public $sourcePath; + /** + * @var string the Web-accessible directory that contains the asset files in this bundle. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by [[AssetManager]] via + * asset publishing). + * + * You can use either a directory or an alias of the directory. + */ + public $basePath; + /** + * @var string the base URL that will be prefixed to the asset files for them to + * be accessed via Web server. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a URL or an alias of the URL. + */ + public $baseUrl; + /** + * @var array list of bundle class names that this bundle depends on. + * + * For example: + * + * ```php + * public $depends = [ + * 'yii\web\YiiAsset', + * 'yii\bootstrap\BootstrapAsset', + * ]; + * ``` + */ + public $depends = []; + /** + * @var array list of JavaScript files that this bundle contains. Each JavaScript file can + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external JavaScript file. + * + * Note that only forward slash "/" can be used as directory separator. + */ + public $js = []; + /** + * @var array list of CSS files that this bundle contains. Each CSS file can + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external CSS file. + * + * Note that only forward slash "/" can be used as directory separator. + */ + public $css = []; + /** + * @var array the options that will be passed to [[\yii\web\View::registerJsFile()]] + * when registering the JS files in this bundle. + */ + public $jsOptions = []; + /** + * @var array the options that will be passed to [[\yii\web\View::registerCssFile()]] + * when registering the CSS files in this bundle. + */ + public $cssOptions = []; + /** + * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle + * is being published. + */ + public $publishOptions = []; - /** - * @param View $view - * @return static the registered asset bundle instance - */ - public static function register($view) - { - return $view->registerAssetBundle(get_called_class()); - } + /** + * @param View $view + * @return static the registered asset bundle instance + */ + public static function register($view) + { + return $view->registerAssetBundle(get_called_class()); + } - /** - * Initializes the bundle. - * If you override this method, make sure you call the parent implementation in the last. - */ - public function init() - { - if ($this->sourcePath !== null) { - $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); - } - if ($this->basePath !== null) { - $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); - } - if ($this->baseUrl !== null) { - $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); - } - } + /** + * Initializes the bundle. + * If you override this method, make sure you call the parent implementation in the last. + */ + public function init() + { + if ($this->sourcePath !== null) { + $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); + } + if ($this->basePath !== null) { + $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); + } + if ($this->baseUrl !== null) { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } + } - /** - * Registers the CSS and JS files with the given view. - * @param \yii\web\View $view the view that the asset files are to be registered with. - */ - public function registerAssetFiles($view) - { - foreach ($this->js as $js) { - if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { - $view->registerJsFile($this->baseUrl . '/' . $js, [], $this->jsOptions); - } else { - $view->registerJsFile($js, [], $this->jsOptions); - } - } - foreach ($this->css as $css) { - if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { - $view->registerCssFile($this->baseUrl . '/' . $css, [], $this->cssOptions); - } else { - $view->registerCssFile($css, [], $this->cssOptions); - } - } - } + /** + * Registers the CSS and JS files with the given view. + * @param \yii\web\View $view the view that the asset files are to be registered with. + */ + public function registerAssetFiles($view) + { + foreach ($this->js as $js) { + if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { + $view->registerJsFile($this->baseUrl . '/' . $js, [], $this->jsOptions); + } else { + $view->registerJsFile($js, [], $this->jsOptions); + } + } + foreach ($this->css as $css) { + if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { + $view->registerCssFile($this->baseUrl . '/' . $css, [], $this->cssOptions); + } else { + $view->registerCssFile($css, [], $this->cssOptions); + } + } + } - /** - * Publishes the asset bundle if its source code is not under Web-accessible directory. - * It will also try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding - * CSS or JS files using [[AssetManager::converter|asset converter]]. - * @param AssetManager $am the asset manager to perform the asset publishing - */ - public function publish($am) - { - if ($this->sourcePath !== null && !isset($this->basePath, $this->baseUrl)) { - list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); - } - $converter = $am->getConverter(); - foreach ($this->js as $i => $js) { - if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { - if (isset($this->basePath, $this->baseUrl)) { - $this->js[$i] = $converter->convert($js, $this->basePath, $this->baseUrl); - } else { - $this->js[$i] = '/' . $js; - } - } - } - foreach ($this->css as $i => $css) { - if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { - if (isset($this->basePath, $this->baseUrl)) { - $this->css[$i] = $converter->convert($css, $this->basePath, $this->baseUrl); - } else { - $this->css[$i] = '/' . $css; - } - } - } - } + /** + * Publishes the asset bundle if its source code is not under Web-accessible directory. + * It will also try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding + * CSS or JS files using [[AssetManager::converter|asset converter]]. + * @param AssetManager $am the asset manager to perform the asset publishing + */ + public function publish($am) + { + if ($this->sourcePath !== null && !isset($this->basePath, $this->baseUrl)) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); + } + $converter = $am->getConverter(); + foreach ($this->js as $i => $js) { + if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $this->js[$i] = $converter->convert($js, $this->basePath, $this->baseUrl); + } else { + $this->js[$i] = '/' . $js; + } + } + } + foreach ($this->css as $i => $css) { + if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $this->css[$i] = $converter->convert($css, $this->basePath, $this->baseUrl); + } else { + $this->css[$i] = '/' . $css; + } + } + } + } } diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php index 79e5e4d3a0c..d679e042dea 100644 --- a/framework/web/AssetConverter.php +++ b/framework/web/AssetConverter.php @@ -21,79 +21,82 @@ */ class AssetConverter extends Component implements AssetConverterInterface { - /** - * @var array the commands that are used to perform the asset conversion. - * The keys are the asset file extension names, and the values are the corresponding - * target script types (either "css" or "js") and the commands used for the conversion. - */ - public $commands = [ - 'less' => ['css', 'lessc {from} {to} --no-color'], - 'scss' => ['css', 'sass {from} {to}'], - 'sass' => ['css', 'sass {from} {to}'], - 'styl' => ['js', 'stylus < {from} > {to}'], - 'coffee' => ['js', 'coffee -p {from} > {to}'], - 'ts' => ['js', 'tsc --out {to} {from}'], - ]; + /** + * @var array the commands that are used to perform the asset conversion. + * The keys are the asset file extension names, and the values are the corresponding + * target script types (either "css" or "js") and the commands used for the conversion. + */ + public $commands = [ + 'less' => ['css', 'lessc {from} {to} --no-color'], + 'scss' => ['css', 'sass {from} {to}'], + 'sass' => ['css', 'sass {from} {to}'], + 'styl' => ['js', 'stylus < {from} > {to}'], + 'coffee' => ['js', 'coffee -p {from} > {to}'], + 'ts' => ['js', 'tsc --out {to} {from}'], + ]; - /** - * Converts a given asset file into a CSS or JS file. - * @param string $asset the asset file path, relative to $basePath - * @param string $basePath the directory the $asset is relative to. - * @return string the converted asset file path, relative to $basePath. - */ - public function convert($asset, $basePath) - { - $pos = strrpos($asset, '.'); - if ($pos !== false) { - $ext = substr($asset, $pos + 1); - if (isset($this->commands[$ext])) { - list ($ext, $command) = $this->commands[$ext]; - $result = substr($asset, 0, $pos + 1) . $ext; - if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { - $this->runCommand($command, $basePath, $asset, $result); - } - return $result; - } - } - return $asset; - } + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @return string the converted asset file path, relative to $basePath. + */ + public function convert($asset, $basePath) + { + $pos = strrpos($asset, '.'); + if ($pos !== false) { + $ext = substr($asset, $pos + 1); + if (isset($this->commands[$ext])) { + list ($ext, $command) = $this->commands[$ext]; + $result = substr($asset, 0, $pos + 1) . $ext; + if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { + $this->runCommand($command, $basePath, $asset, $result); + } - /** - * Runs a command to convert asset files. - * @param string $command the command to run - * @param string $basePath asset base path and command working directory - * @param string $asset the name of the asset file - * @param string $result the name of the file to be generated by the converter command - * @return boolean true on success, false on failure. Failures will be logged. - * @throws \yii\base\Exception when the command fails and YII_DEBUG is true. - * In production mode the error will be logged. - */ - protected function runCommand($command, $basePath, $asset, $result) - { - $command = strtr($command, [ - '{from}' => escapeshellarg("$basePath/$asset"), - '{to}' => escapeshellarg("$basePath/$result"), - ]); - $descriptor = [ - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - $pipes = []; - $proc = proc_open($command, $descriptor, $pipes, $basePath); - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - foreach ($pipes as $pipe) { - fclose($pipe); - } - $status = proc_close($proc); + return $result; + } + } - if ($status === 0) { - Yii::trace("Converted $asset into $result:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", __METHOD__); - } elseif (YII_DEBUG) { - throw new Exception("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr"); - } else { - Yii::error("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr"); - } - return $status === 0; - } + return $asset; + } + + /** + * Runs a command to convert asset files. + * @param string $command the command to run + * @param string $basePath asset base path and command working directory + * @param string $asset the name of the asset file + * @param string $result the name of the file to be generated by the converter command + * @return boolean true on success, false on failure. Failures will be logged. + * @throws \yii\base\Exception when the command fails and YII_DEBUG is true. + * In production mode the error will be logged. + */ + protected function runCommand($command, $basePath, $asset, $result) + { + $command = strtr($command, [ + '{from}' => escapeshellarg("$basePath/$asset"), + '{to}' => escapeshellarg("$basePath/$result"), + ]); + $descriptor = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $pipes = []; + $proc = proc_open($command, $descriptor, $pipes, $basePath); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + foreach ($pipes as $pipe) { + fclose($pipe); + } + $status = proc_close($proc); + + if ($status === 0) { + Yii::trace("Converted $asset into $result:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr", __METHOD__); + } elseif (YII_DEBUG) { + throw new Exception("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr"); + } else { + Yii::error("AssetConverter command '$command' failed with exit code $status:\nSTDOUT:\n$stdout\nSTDERR:\n$stderr"); + } + + return $status === 0; + } } diff --git a/framework/web/AssetConverterInterface.php b/framework/web/AssetConverterInterface.php index 51309c61f88..8a0e6797cd5 100644 --- a/framework/web/AssetConverterInterface.php +++ b/framework/web/AssetConverterInterface.php @@ -15,11 +15,11 @@ */ interface AssetConverterInterface { - /** - * Converts a given asset file into a CSS or JS file. - * @param string $asset the asset file path, relative to $basePath - * @param string $basePath the directory the $asset is relative to. - * @return string the converted asset file path, relative to $basePath. - */ - public function convert($asset, $basePath); + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @return string the converted asset file path, relative to $basePath. + */ + public function convert($asset, $basePath); } diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index c600223ddfa..745fdf185dc 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -40,312 +40,314 @@ */ class AssetManager extends Component { - /** - * @var array list of available asset bundles. The keys are the class names (without leading backslash) - * of the asset bundles, and the values are either the configuration arrays for creating the [[AssetBundle]] - * objects or the corresponding asset bundle instances. For example, the following code disables - * the bootstrap css file used by Bootstrap widgets (because you want to use your own styles): - * - * ~~~ - * [ - * 'yii\bootstrap\BootstrapAsset' => [ - * 'css' => [], - * ], - * ] - * ~~~ - */ - public $bundles = []; - /** - * @return string the root directory storing the published asset files. - */ - public $basePath = '@webroot/assets'; - /** - * @return string the base URL through which the published asset files can be accessed. - */ - public $baseUrl = '@web/assets'; - /** - * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning - * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published - * assets will always be consistent with the source assets and there is no copy operation required. - * This is especially useful during development. - * - * However, there are special requirements for hosting environments in order to use symbolic links. - * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. - * - * Moreover, some Web servers need to be properly configured so that the linked assets are accessible - * to Web users. For example, for Apache Web server, the following configuration directive should be added - * for the Web folder: - * - * ~~~ - * Options FollowSymLinks - * ~~~ - */ - public $linkAssets = false; - /** - * @var integer the permission to be set for newly published asset files. - * This value will be used by PHP chmod() function. No umask will be applied. - * If not set, the permission will be determined by the current environment. - */ - public $fileMode; - /** - * @var integer the permission to be set for newly generated asset directories. - * This value will be used by PHP chmod() function. No umask will be applied. - * Defaults to 0775, meaning the directory is read-writable by owner and group, - * but read-only for other users. - */ - public $dirMode = 0775; + /** + * @var array list of available asset bundles. The keys are the class names (without leading backslash) + * of the asset bundles, and the values are either the configuration arrays for creating the [[AssetBundle]] + * objects or the corresponding asset bundle instances. For example, the following code disables + * the bootstrap css file used by Bootstrap widgets (because you want to use your own styles): + * + * ~~~ + * [ + * 'yii\bootstrap\BootstrapAsset' => [ + * 'css' => [], + * ], + * ] + * ~~~ + */ + public $bundles = []; + /** + * @return string the root directory storing the published asset files. + */ + public $basePath = '@webroot/assets'; + /** + * @return string the base URL through which the published asset files can be accessed. + */ + public $baseUrl = '@web/assets'; + /** + * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning + * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published + * assets will always be consistent with the source assets and there is no copy operation required. + * This is especially useful during development. + * + * However, there are special requirements for hosting environments in order to use symbolic links. + * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. + * + * Moreover, some Web servers need to be properly configured so that the linked assets are accessible + * to Web users. For example, for Apache Web server, the following configuration directive should be added + * for the Web folder: + * + * ~~~ + * Options FollowSymLinks + * ~~~ + */ + public $linkAssets = false; + /** + * @var integer the permission to be set for newly published asset files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly generated asset directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; - /** - * Initializes the component. - * @throws InvalidConfigException if [[basePath]] is invalid - */ - public function init() - { - parent::init(); - $this->basePath = Yii::getAlias($this->basePath); - if (!is_dir($this->basePath)) { - throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); - } elseif (!is_writable($this->basePath)) { - throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); - } else { - $this->basePath = realpath($this->basePath); - } - $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); - } + /** + * Initializes the component. + * @throws InvalidConfigException if [[basePath]] is invalid + */ + public function init() + { + parent::init(); + $this->basePath = Yii::getAlias($this->basePath); + if (!is_dir($this->basePath)) { + throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); + } elseif (!is_writable($this->basePath)) { + throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); + } else { + $this->basePath = realpath($this->basePath); + } + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } - /** - * Returns the named asset bundle. - * - * This method will first look for the bundle in [[bundles]]. If not found, - * it will treat `$name` as the class of the asset bundle and create a new instance of it. - * - * @param string $name the class name of the asset bundle - * @param boolean $publish whether to publish the asset files in the asset bundle before it is returned. - * If you set this false, you must manually call `AssetBundle::publish()` to publish the asset files. - * @return AssetBundle the asset bundle instance - * @throws InvalidConfigException if $name does not refer to a valid asset bundle - */ - public function getBundle($name, $publish = true) - { - if (isset($this->bundles[$name])) { - if ($this->bundles[$name] instanceof AssetBundle) { - return $this->bundles[$name]; - } elseif (is_array($this->bundles[$name])) { - $bundle = Yii::createObject(array_merge(['class' => $name], $this->bundles[$name])); - } else { - throw new InvalidConfigException("Invalid asset bundle: $name"); - } - } else { - $bundle = Yii::createObject($name); - } - if ($publish) { - /** @var AssetBundle $bundle */ - $bundle->publish($this); - } - return $this->bundles[$name] = $bundle; - } + /** + * Returns the named asset bundle. + * + * This method will first look for the bundle in [[bundles]]. If not found, + * it will treat `$name` as the class of the asset bundle and create a new instance of it. + * + * @param string $name the class name of the asset bundle + * @param boolean $publish whether to publish the asset files in the asset bundle before it is returned. + * If you set this false, you must manually call `AssetBundle::publish()` to publish the asset files. + * @return AssetBundle the asset bundle instance + * @throws InvalidConfigException if $name does not refer to a valid asset bundle + */ + public function getBundle($name, $publish = true) + { + if (isset($this->bundles[$name])) { + if ($this->bundles[$name] instanceof AssetBundle) { + return $this->bundles[$name]; + } elseif (is_array($this->bundles[$name])) { + $bundle = Yii::createObject(array_merge(['class' => $name], $this->bundles[$name])); + } else { + throw new InvalidConfigException("Invalid asset bundle: $name"); + } + } else { + $bundle = Yii::createObject($name); + } + if ($publish) { + /** @var AssetBundle $bundle */ + $bundle->publish($this); + } - private $_converter; + return $this->bundles[$name] = $bundle; + } - /** - * Returns the asset converter. - * @return AssetConverterInterface the asset converter. - */ - public function getConverter() - { - if ($this->_converter === null) { - $this->_converter = Yii::createObject(AssetConverter::className()); - } elseif (is_array($this->_converter) || is_string($this->_converter)) { - if (is_array($this->_converter) && !isset($this->_converter['class'])) { - $this->_converter['class'] = AssetConverter::className(); - } - $this->_converter = Yii::createObject($this->_converter); - } - return $this->_converter; - } + private $_converter; - /** - * Sets the asset converter. - * @param array|AssetConverterInterface $value the asset converter. This can be either - * an object implementing the [[AssetConverterInterface]], or a configuration - * array that can be used to create the asset converter object. - */ - public function setConverter($value) - { - $this->_converter = $value; - } + /** + * Returns the asset converter. + * @return AssetConverterInterface the asset converter. + */ + public function getConverter() + { + if ($this->_converter === null) { + $this->_converter = Yii::createObject(AssetConverter::className()); + } elseif (is_array($this->_converter) || is_string($this->_converter)) { + if (is_array($this->_converter) && !isset($this->_converter['class'])) { + $this->_converter['class'] = AssetConverter::className(); + } + $this->_converter = Yii::createObject($this->_converter); + } - /** - * @var array published assets - */ - private $_published = []; + return $this->_converter; + } - /** - * Publishes a file or a directory. - * - * This method will copy the specified file or directory to [[basePath]] so that - * it can be accessed via the Web server. - * - * If the asset is a file, its file modification time will be checked to avoid - * unnecessary file copying. - * - * If the asset is a directory, all files and subdirectories under it will be published recursively. - * Note, in case $forceCopy is false the method only checks the existence of the target - * directory to avoid repetitive copying (which is very expensive). - * - * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." - * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option - * as explained in the `$options` parameter. - * - * Note: On rare scenario, a race condition can develop that will lead to a - * one-time-manifestation of a non-critical problem in the creation of the directory - * that holds the published assets. This problem can be avoided altogether by 'requesting' - * in advance all the resources that are supposed to trigger a 'publish()' call, and doing - * that in the application deployment phase, before system goes live. See more in the following - * discussion: http://code.google.com/p/yii/issues/detail?id=2579 - * - * @param string $path the asset (file or directory) to be published - * @param array $options the options to be applied when publishing a directory. - * The following options are supported: - * - * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. - * This option is used only when publishing a directory. If the callback returns false, the copy - * operation for the sub-directory or file will be cancelled. - * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or - * file to be copied from, while `$to` is the copy target. - * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. - * This option is used only when publishing a directory. The signature of the callback is similar to that - * of `beforeCopy`. - * - forceCopy: boolean, whether the directory being published should be copied even if - * it is found in the target directory. This option is used only when publishing a directory. - * You may want to set this to be true during the development stage to make sure the published - * directory is always up-to-date. Do not set this to true on production servers as it will - * significantly degrade the performance. - * @return array the path (directory or file path) and the URL that the asset is published as. - * @throws InvalidParamException if the asset to be published does not exist. - */ - public function publish($path, $options = []) - { - $path = Yii::getAlias($path); + /** + * Sets the asset converter. + * @param array|AssetConverterInterface $value the asset converter. This can be either + * an object implementing the [[AssetConverterInterface]], or a configuration + * array that can be used to create the asset converter object. + */ + public function setConverter($value) + { + $this->_converter = $value; + } - if (isset($this->_published[$path])) { - return $this->_published[$path]; - } + /** + * @var array published assets + */ + private $_published = []; - if (!is_string($path) || ($src = realpath($path)) === false) { - throw new InvalidParamException("The file or directory to be published does not exist: $path"); - } + /** + * Publishes a file or a directory. + * + * This method will copy the specified file or directory to [[basePath]] so that + * it can be accessed via the Web server. + * + * If the asset is a file, its file modification time will be checked to avoid + * unnecessary file copying. + * + * If the asset is a directory, all files and subdirectories under it will be published recursively. + * Note, in case $forceCopy is false the method only checks the existence of the target + * directory to avoid repetitive copying (which is very expensive). + * + * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." + * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option + * as explained in the `$options` parameter. + * + * Note: On rare scenario, a race condition can develop that will lead to a + * one-time-manifestation of a non-critical problem in the creation of the directory + * that holds the published assets. This problem can be avoided altogether by 'requesting' + * in advance all the resources that are supposed to trigger a 'publish()' call, and doing + * that in the application deployment phase, before system goes live. See more in the following + * discussion: http://code.google.com/p/yii/issues/detail?id=2579 + * + * @param string $path the asset (file or directory) to be published + * @param array $options the options to be applied when publishing a directory. + * The following options are supported: + * + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * This option is used only when publishing a directory. If the callback returns false, the copy + * operation for the sub-directory or file will be cancelled. + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * This option is used only when publishing a directory. The signature of the callback is similar to that + * of `beforeCopy`. + * - forceCopy: boolean, whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * You may want to set this to be true during the development stage to make sure the published + * directory is always up-to-date. Do not set this to true on production servers as it will + * significantly degrade the performance. + * @return array the path (directory or file path) and the URL that the asset is published as. + * @throws InvalidParamException if the asset to be published does not exist. + */ + public function publish($path, $options = []) + { + $path = Yii::getAlias($path); - if (is_file($src)) { - $dir = $this->hash(dirname($src) . filemtime($src)); - $fileName = basename($src); - $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; - $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; + if (isset($this->_published[$path])) { + return $this->_published[$path]; + } - if (!is_dir($dstDir)) { - FileHelper::createDirectory($dstDir, $this->dirMode, true); - } + if (!is_string($path) || ($src = realpath($path)) === false) { + throw new InvalidParamException("The file or directory to be published does not exist: $path"); + } - if ($this->linkAssets) { - if (!is_file($dstFile)) { - symlink($src, $dstFile); - } - } elseif (@filemtime($dstFile) < @filemtime($src)) { - copy($src, $dstFile); - if ($this->fileMode !== null) { - @chmod($dstFile, $this->fileMode); - } - } + if (is_file($src)) { + $dir = $this->hash(dirname($src) . filemtime($src)); + $fileName = basename($src); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; - return $this->_published[$path] = [$dstFile, $this->baseUrl . "/$dir/$fileName"]; - } else { - $dir = $this->hash($src . filemtime($src)); - $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; - if ($this->linkAssets) { - if (!is_dir($dstDir)) { - symlink($src, $dstDir); - } - } elseif (!is_dir($dstDir) || !empty($options['forceCopy'])) { - $opts = [ - 'dirMode' => $this->dirMode, - 'fileMode' => $this->fileMode, - ]; - if (isset($options['beforeCopy'])) { - $opts['beforeCopy'] = $options['beforeCopy']; - } else { - $opts['beforeCopy'] = function ($from, $to) { - return strncmp(basename($from), '.', 1) !== 0; - }; - } - if (isset($options['afterCopy'])) { - $opts['afterCopy'] = $options['afterCopy']; - } - FileHelper::copyDirectory($src, $dstDir, $opts); - } + if (!is_dir($dstDir)) { + FileHelper::createDirectory($dstDir, $this->dirMode, true); + } - return $this->_published[$path] = [$dstDir, $this->baseUrl . '/' . $dir]; - } - } + if ($this->linkAssets) { + if (!is_file($dstFile)) { + symlink($src, $dstFile); + } + } elseif (@filemtime($dstFile) < @filemtime($src)) { + copy($src, $dstFile); + if ($this->fileMode !== null) { + @chmod($dstFile, $this->fileMode); + } + } - /** - * Returns the published path of a file path. - * This method does not perform any publishing. It merely tells you - * if the file or directory is published, where it will go. - * @param string $path directory or file path being published - * @return string the published file path. False if the file or directory does not exist - */ - public function getPublishedPath($path) - { - $path = Yii::getAlias($path); + return $this->_published[$path] = [$dstFile, $this->baseUrl . "/$dir/$fileName"]; + } else { + $dir = $this->hash($src . filemtime($src)); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + if ($this->linkAssets) { + if (!is_dir($dstDir)) { + symlink($src, $dstDir); + } + } elseif (!is_dir($dstDir) || !empty($options['forceCopy'])) { + $opts = [ + 'dirMode' => $this->dirMode, + 'fileMode' => $this->fileMode, + ]; + if (isset($options['beforeCopy'])) { + $opts['beforeCopy'] = $options['beforeCopy']; + } else { + $opts['beforeCopy'] = function ($from, $to) { + return strncmp(basename($from), '.', 1) !== 0; + }; + } + if (isset($options['afterCopy'])) { + $opts['afterCopy'] = $options['afterCopy']; + } + FileHelper::copyDirectory($src, $dstDir, $opts); + } - if (isset($this->_published[$path])) { - return $this->_published[$path][0]; - } - if (is_string($path) && ($path = realpath($path)) !== false) { - $base = $this->basePath . DIRECTORY_SEPARATOR; - if (is_file($path)) { - return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); - } else { - return $base . $this->hash($path . filemtime($path)); - } - } else { - return false; - } - } + return $this->_published[$path] = [$dstDir, $this->baseUrl . '/' . $dir]; + } + } - /** - * Returns the URL of a published file path. - * This method does not perform any publishing. It merely tells you - * if the file path is published, what the URL will be to access it. - * @param string $path directory or file path being published - * @return string the published URL for the file or directory. False if the file or directory does not exist. - */ - public function getPublishedUrl($path) - { - $path = Yii::getAlias($path); - - if (isset($this->_published[$path])) { - return $this->_published[$path][1]; - } - if (is_string($path) && ($path = realpath($path)) !== false) { - if (is_file($path)) { - return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path); - } else { - return $this->baseUrl . '/' . $this->hash($path . filemtime($path)); - } - } else { - return false; - } - } + /** + * Returns the published path of a file path. + * This method does not perform any publishing. It merely tells you + * if the file or directory is published, where it will go. + * @param string $path directory or file path being published + * @return string the published file path. False if the file or directory does not exist + */ + public function getPublishedPath($path) + { + $path = Yii::getAlias($path); - /** - * Generate a CRC32 hash for the directory path. Collisions are higher - * than MD5 but generates a much smaller hash string. - * @param string $path string to be hashed. - * @return string hashed string. - */ - protected function hash($path) - { - return sprintf('%x', crc32($path . Yii::getVersion())); - } + if (isset($this->_published[$path])) { + return $this->_published[$path][0]; + } + if (is_string($path) && ($path = realpath($path)) !== false) { + $base = $this->basePath . DIRECTORY_SEPARATOR; + if (is_file($path)) { + return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + } else { + return $base . $this->hash($path . filemtime($path)); + } + } else { + return false; + } + } + + /** + * Returns the URL of a published file path. + * This method does not perform any publishing. It merely tells you + * if the file path is published, what the URL will be to access it. + * @param string $path directory or file path being published + * @return string the published URL for the file or directory. False if the file or directory does not exist. + */ + public function getPublishedUrl($path) + { + $path = Yii::getAlias($path); + + if (isset($this->_published[$path])) { + return $this->_published[$path][1]; + } + if (is_string($path) && ($path = realpath($path)) !== false) { + if (is_file($path)) { + return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path); + } else { + return $this->baseUrl . '/' . $this->hash($path . filemtime($path)); + } + } else { + return false; + } + } + + /** + * Generate a CRC32 hash for the directory path. Collisions are higher + * than MD5 but generates a much smaller hash string. + * @param string $path string to be hashed. + * @return string hashed string. + */ + protected function hash($path) + { + return sprintf('%x', crc32($path . Yii::getVersion())); + } } diff --git a/framework/web/BadRequestHttpException.php b/framework/web/BadRequestHttpException.php index 6e596dab1aa..b5228295f19 100644 --- a/framework/web/BadRequestHttpException.php +++ b/framework/web/BadRequestHttpException.php @@ -21,14 +21,14 @@ */ class BadRequestHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(400, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(400, $message, $code, $previous); + } } diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index 7b4a98d4074..733fa27c8bf 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -38,81 +38,82 @@ */ class CacheSession extends Session { - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * The session data will be stored using this cache object. - * - * After the CacheSession object is created, if you want to change this property, - * you should only assign it with a cache object. - */ - public $cache = 'cache'; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * The session data will be stored using this cache object. + * + * After the CacheSession object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; - /** - * Initializes the application component. - */ - public function init() - { - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } - if (!$this->cache instanceof Cache) { - throw new InvalidConfigException('CacheSession::cache must refer to the application component ID of a cache object.'); - } - parent::init(); - } + /** + * Initializes the application component. + */ + public function init() + { + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException('CacheSession::cache must refer to the application component ID of a cache object.'); + } + parent::init(); + } - /** - * Returns a value indicating whether to use custom session storage. - * This method overrides the parent implementation and always returns true. - * @return boolean whether to use custom storage. - */ - public function getUseCustomStorage() - { - return true; - } + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } - /** - * Session read handler. - * Do not call this method directly. - * @param string $id session ID - * @return string the session data - */ - public function readSession($id) - { - $data = $this->cache->get($this->calculateKey($id)); - return $data === false ? '' : $data; - } + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $data = $this->cache->get($this->calculateKey($id)); - /** - * Session write handler. - * Do not call this method directly. - * @param string $id session ID - * @param string $data session data - * @return boolean whether session write is successful - */ - public function writeSession($id, $data) - { - return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); - } + return $data === false ? '' : $data; + } - /** - * Session destroy handler. - * Do not call this method directly. - * @param string $id session ID - * @return boolean whether session is destroyed successfully - */ - public function destroySession($id) - { - return $this->cache->delete($this->calculateKey($id)); - } + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); + } - /** - * Generates a unique key used for storing session data in cache. - * @param string $id session variable name - * @return mixed a safe cache key associated with the session variable name - */ - protected function calculateKey($id) - { - return [__CLASS__, $id]; - } + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + return $this->cache->delete($this->calculateKey($id)); + } + + /** + * Generates a unique key used for storing session data in cache. + * @param string $id session variable name + * @return mixed a safe cache key associated with the session variable name + */ + protected function calculateKey($id) + { + return [__CLASS__, $id]; + } } diff --git a/framework/web/CompositeUrlRule.php b/framework/web/CompositeUrlRule.php index 2382ec57f84..e2aa2e9a6bb 100644 --- a/framework/web/CompositeUrlRule.php +++ b/framework/web/CompositeUrlRule.php @@ -20,54 +20,56 @@ */ abstract class CompositeUrlRule extends Object implements UrlRuleInterface { - /** - * @var UrlRuleInterface[] the URL rules contained in this composite rule. - * This property is set in [[init()]] by the return value of [[createRules()]]. - */ - protected $rules = []; + /** + * @var UrlRuleInterface[] the URL rules contained in this composite rule. + * This property is set in [[init()]] by the return value of [[createRules()]]. + */ + protected $rules = []; + /** + * Creates the URL rules that should be contained within this composite rule. + * @return UrlRuleInterface[] the URL rules + */ + abstract protected function createRules(); - /** - * Creates the URL rules that should be contained within this composite rule. - * @return UrlRuleInterface[] the URL rules - */ - abstract protected function createRules(); + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + $this->rules = $this->createRules(); + } - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - $this->rules = $this->createRules(); - } + /** + * @inheritdoc + */ + public function parseRequest($manager, $request) + { + foreach ($this->rules as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($result = $rule->parseRequest($manager, $request)) !== false) { + Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); - /** - * @inheritdoc - */ - public function parseRequest($manager, $request) - { - foreach ($this->rules as $rule) { - /** @var \yii\web\UrlRule $rule */ - if (($result = $rule->parseRequest($manager, $request)) !== false) { - Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__); - return $result; - } - } - return false; - } + return $result; + } + } - /** - * @inheritdoc - */ - public function createUrl($manager, $route, $params) - { - foreach ($this->rules as $rule) { - /** @var \yii\web\UrlRule $rule */ - if (($url = $rule->createUrl($manager, $route, $params)) !== false) { - return $url; - } - } - return false; - } + return false; + } + + /** + * @inheritdoc + */ + public function createUrl($manager, $route, $params) + { + foreach ($this->rules as $rule) { + /** @var \yii\web\UrlRule $rule */ + if (($url = $rule->createUrl($manager, $route, $params)) !== false) { + return $url; + } + } + + return false; + } } diff --git a/framework/web/ConflictHttpException.php b/framework/web/ConflictHttpException.php index 6fa3f57bab4..75b63c51741 100644 --- a/framework/web/ConflictHttpException.php +++ b/framework/web/ConflictHttpException.php @@ -16,14 +16,14 @@ */ class ConflictHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(409, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(409, $message, $code, $previous); + } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 3535c0b4b67..6bd7db5eadd 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -19,188 +19,188 @@ */ class Controller extends \yii\base\Controller { - /** - * @var boolean whether to enable CSRF validation for the actions in this controller. - * CSRF validation is enabled only when both this property and [[Request::enableCsrfValidation]] are true. - */ - public $enableCsrfValidation = true; - /** - * @var array the parameters bound to the current action. - */ - public $actionParams = []; + /** + * @var boolean whether to enable CSRF validation for the actions in this controller. + * CSRF validation is enabled only when both this property and [[Request::enableCsrfValidation]] are true. + */ + public $enableCsrfValidation = true; + /** + * @var array the parameters bound to the current action. + */ + public $actionParams = []; + /** + * Renders a view in response to an AJAX request. + * + * This method is similar to [[renderPartial()]] except that it will inject into + * the rendering result with JS/CSS scripts and files which are registered with the view. + * For this reason, you should use this method instead of [[renderPartial()]] to render + * a view to respond to an AJAX request. + * + * @param string $view the view name. Please refer to [[render()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + */ + public function renderAjax($view, $params = []) + { + return $this->getView()->renderAjax($view, $params, $this); + } - /** - * Renders a view in response to an AJAX request. - * - * This method is similar to [[renderPartial()]] except that it will inject into - * the rendering result with JS/CSS scripts and files which are registered with the view. - * For this reason, you should use this method instead of [[renderPartial()]] to render - * a view to respond to an AJAX request. - * - * @param string $view the view name. Please refer to [[render()]] on how to specify a view name. - * @param array $params the parameters (name-value pairs) that should be made available in the view. - * @return string the rendering result. - */ - public function renderAjax($view, $params = []) - { - return $this->getView()->renderAjax($view, $params, $this); - } + /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will check the parameter names that the action requires and return + * the provided parameters according to the requirement. If there is any missing parameter, + * an exception will be thrown. + * @param \yii\base\Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws HttpException if there are missing or invalid parameters. + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } - /** - * Binds the parameters to the action. - * This method is invoked by [[Action]] when it begins to run with the given parameters. - * This method will check the parameter names that the action requires and return - * the provided parameters according to the requirement. If there is any missing parameter, - * an exception will be thrown. - * @param \yii\base\Action $action the action to be bound with parameters - * @param array $params the parameters to be bound to the action - * @return array the valid parameters that the action can run with. - * @throws HttpException if there are missing or invalid parameters. - */ - public function bindActionParams($action, $params) - { - if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($this, $action->actionMethod); - } else { - $method = new \ReflectionMethod($action, 'run'); - } + $args = []; + $missing = []; + $actionParams = []; + foreach ($method->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $params)) { + if ($param->isArray()) { + $args[] = $actionParams[$name] = is_array($params[$name]) ? $params[$name] : [$params[$name]]; + } elseif (!is_array($params[$name])) { + $args[] = $actionParams[$name] = $params[$name]; + } else { + throw new BadRequestHttpException(Yii::t('yii', 'Invalid data received for parameter "{param}".', [ + 'param' => $name, + ])); + } + unset($params[$name]); + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $actionParams[$name] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } - $args = []; - $missing = []; - $actionParams = []; - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - if ($param->isArray()) { - $args[] = $actionParams[$name] = is_array($params[$name]) ? $params[$name] : [$params[$name]]; - } elseif (!is_array($params[$name])) { - $args[] = $actionParams[$name] = $params[$name]; - } else { - throw new BadRequestHttpException(Yii::t('yii', 'Invalid data received for parameter "{param}".', [ - 'param' => $name, - ])); - } - unset($params[$name]); - } elseif ($param->isDefaultValueAvailable()) { - $args[] = $actionParams[$name] = $param->getDefaultValue(); - } else { - $missing[] = $name; - } - } + if (!empty($missing)) { + throw new BadRequestHttpException(Yii::t('yii', 'Missing required parameters: {params}', [ + 'params' => implode(', ', $missing), + ])); + } - if (!empty($missing)) { - throw new BadRequestHttpException(Yii::t('yii', 'Missing required parameters: {params}', [ - 'params' => implode(', ', $missing), - ])); - } + $this->actionParams = $actionParams; - $this->actionParams = $actionParams; + return $args; + } - return $args; - } + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + if ($this->enableCsrfValidation && Yii::$app->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) { + throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.')); + } - /** - * @inheritdoc - */ - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - if ($this->enableCsrfValidation && Yii::$app->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) { - throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.')); - } - return true; - } else { - return false; - } - } + return true; + } else { + return false; + } + } - /** - * Redirects the browser to the specified URL. - * This method is a shortcut to [[Response::redirect()]]. - * - * You can use it in an action by returning the [[Response]] directly: - * - * ```php - * // stop executing this action and redirect to login page - * return $this->redirect(['login']); - * ``` - * - * @param string|array $url the URL to be redirected to. This can be in one of the following formats: - * - * - a string representing a URL (e.g. "http://example.com") - * - a string representing a URL alias (e.g. "@example.com") - * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`) - * [[Url::to()]] will be used to convert the array into a URL. - * - * Any relative URL will be converted into an absolute one by prepending it with the host info - * of the current request. - * - * @param integer $statusCode the HTTP status code. Defaults to 302. - * See - * for details about HTTP status code - * @return Response the current response object - */ - public function redirect($url, $statusCode = 302) - { - return Yii::$app->getResponse()->redirect(Url::to($url), $statusCode); - } + /** + * Redirects the browser to the specified URL. + * This method is a shortcut to [[Response::redirect()]]. + * + * You can use it in an action by returning the [[Response]] directly: + * + * ```php + * // stop executing this action and redirect to login page + * return $this->redirect(['login']); + * ``` + * + * @param string|array $url the URL to be redirected to. This can be in one of the following formats: + * + * - a string representing a URL (e.g. "http://example.com") + * - a string representing a URL alias (e.g. "@example.com") + * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`) + * [[Url::to()]] will be used to convert the array into a URL. + * + * Any relative URL will be converted into an absolute one by prepending it with the host info + * of the current request. + * + * @param integer $statusCode the HTTP status code. Defaults to 302. + * See + * for details about HTTP status code + * @return Response the current response object + */ + public function redirect($url, $statusCode = 302) + { + return Yii::$app->getResponse()->redirect(Url::to($url), $statusCode); + } - /** - * Redirects the browser to the home page. - * - * You can use this method in an action by returning the [[Response]] directly: - * - * ```php - * // stop executing this action and redirect to home page - * return $this->goHome(); - * ``` - * - * @return Response the current response object - */ - public function goHome() - { - return Yii::$app->getResponse()->redirect(Yii::$app->getHomeUrl()); - } + /** + * Redirects the browser to the home page. + * + * You can use this method in an action by returning the [[Response]] directly: + * + * ```php + * // stop executing this action and redirect to home page + * return $this->goHome(); + * ``` + * + * @return Response the current response object + */ + public function goHome() + { + return Yii::$app->getResponse()->redirect(Yii::$app->getHomeUrl()); + } - /** - * Redirects the browser to the last visited page. - * - * You can use this method in an action by returning the [[Response]] directly: - * - * ```php - * // stop executing this action and redirect to last visited page - * return $this->goBack(); - * ``` - * - * @param string|array $defaultUrl the default return URL in case it was not set previously. - * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. - * Please refer to [[User::setReturnUrl()]] on accepted format of the URL. - * @return Response the current response object - * @see User::getReturnUrl() - */ - public function goBack($defaultUrl = null) - { - return Yii::$app->getResponse()->redirect(Yii::$app->getUser()->getReturnUrl($defaultUrl)); - } + /** + * Redirects the browser to the last visited page. + * + * You can use this method in an action by returning the [[Response]] directly: + * + * ```php + * // stop executing this action and redirect to last visited page + * return $this->goBack(); + * ``` + * + * @param string|array $defaultUrl the default return URL in case it was not set previously. + * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. + * Please refer to [[User::setReturnUrl()]] on accepted format of the URL. + * @return Response the current response object + * @see User::getReturnUrl() + */ + public function goBack($defaultUrl = null) + { + return Yii::$app->getResponse()->redirect(Yii::$app->getUser()->getReturnUrl($defaultUrl)); + } - /** - * Refreshes the current page. - * This method is a shortcut to [[Response::refresh()]]. - * - * You can use it in an action by returning the [[Response]] directly: - * - * ```php - * // stop executing this action and refresh the current page - * return $this->refresh(); - * ``` - * - * @param string $anchor the anchor that should be appended to the redirection URL. - * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. - * @return Response the response object itself - */ - public function refresh($anchor = '') - { - return Yii::$app->getResponse()->redirect(Yii::$app->getRequest()->getUrl() . $anchor); - } + /** + * Refreshes the current page. + * This method is a shortcut to [[Response::refresh()]]. + * + * You can use it in an action by returning the [[Response]] directly: + * + * ```php + * // stop executing this action and refresh the current page + * return $this->refresh(); + * ``` + * + * @param string $anchor the anchor that should be appended to the redirection URL. + * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. + * @return Response the response object itself + */ + public function refresh($anchor = '') + { + return Yii::$app->getResponse()->redirect(Yii::$app->getRequest()->getUrl() . $anchor); + } } diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php index 8cbb412dd7b..c3518722c22 100644 --- a/framework/web/Cookie.php +++ b/framework/web/Cookie.php @@ -15,51 +15,51 @@ */ class Cookie extends \yii\base\Object { - /** - * @var string name of the cookie - */ - public $name; - /** - * @var string value of the cookie - */ - public $value = ''; - /** - * @var string domain of the cookie - */ - public $domain = ''; - /** - * @var integer the timestamp at which the cookie expires. This is the server timestamp. - * Defaults to 0, meaning "until the browser is closed". - */ - public $expire = 0; - /** - * @var string the path on the server in which the cookie will be available on. The default is '/'. - */ - public $path = '/'; - /** - * @var boolean whether cookie should be sent via secure connection - */ - public $secure = false; - /** - * @var boolean whether the cookie should be accessible only through the HTTP protocol. - * By setting this property to true, the cookie will not be accessible by scripting languages, - * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. - */ - public $httpOnly = false; + /** + * @var string name of the cookie + */ + public $name; + /** + * @var string value of the cookie + */ + public $value = ''; + /** + * @var string domain of the cookie + */ + public $domain = ''; + /** + * @var integer the timestamp at which the cookie expires. This is the server timestamp. + * Defaults to 0, meaning "until the browser is closed". + */ + public $expire = 0; + /** + * @var string the path on the server in which the cookie will be available on. The default is '/'. + */ + public $path = '/'; + /** + * @var boolean whether cookie should be sent via secure connection + */ + public $secure = false; + /** + * @var boolean whether the cookie should be accessible only through the HTTP protocol. + * By setting this property to true, the cookie will not be accessible by scripting languages, + * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. + */ + public $httpOnly = false; - /** - * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. - * - * ~~~ - * if (isset($request->cookies['name'])) { - * $value = (string)$request->cookies['name']; - * } - * ~~~ - * - * @return string The value of the cookie. If the value property is null, an empty string will be returned. - */ - public function __toString() - { - return (string)$this->value; - } + /** + * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. + * + * ~~~ + * if (isset($request->cookies['name'])) { + * $value = (string) $request->cookies['name']; + * } + * ~~~ + * + * @return string The value of the cookie. If the value property is null, an empty string will be returned. + */ + public function __toString() + { + return (string) $this->value; + } } diff --git a/framework/web/CookieCollection.php b/framework/web/CookieCollection.php index 475306c657d..cbdd5bea683 100644 --- a/framework/web/CookieCollection.php +++ b/framework/web/CookieCollection.php @@ -24,204 +24,204 @@ */ class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable { - /** - * @var boolean whether this collection is read only. - */ - public $readOnly = false; - - /** - * @var Cookie[] the cookies in this collection (indexed by the cookie names) - */ - private $_cookies = []; - - /** - * Constructor. - * @param array $cookies the cookies that this collection initially contains. This should be - * an array of name-value pairs.s - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($cookies = [], $config = []) - { - $this->_cookies = $cookies; - parent::__construct($config); - } - - /** - * Returns an iterator for traversing the cookies in the collection. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the collection. - * @return ArrayIterator an iterator for traversing the cookies in the collection. - */ - public function getIterator() - { - return new ArrayIterator($this->_cookies); - } - - /** - * Returns the number of cookies in the collection. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($collection)`. - * @return integer the number of cookies in the collection. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the number of cookies in the collection. - * @return integer the number of cookies in the collection. - */ - public function getCount() - { - return count($this->_cookies); - } - - /** - * Returns the cookie with the specified name. - * @param string $name the cookie name - * @return Cookie the cookie with the specified name. Null if the named cookie does not exist. - * @see getValue() - */ - public function get($name) - { - return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; - } - - /** - * Returns the value of the named cookie. - * @param string $name the cookie name - * @param mixed $defaultValue the value that should be returned when the named cookie does not exist. - * @return mixed the value of the named cookie. - * @see get() - */ - public function getValue($name, $defaultValue = null) - { - return isset($this->_cookies[$name]) ? $this->_cookies[$name]->value : $defaultValue; - } - - /** - * Returns whether there is a cookie with the specified name. - * @param string $name the cookie name - * @return boolean whether the named cookie exists - */ - public function has($name) - { - return isset($this->_cookies[$name]); - } - - /** - * Adds a cookie to the collection. - * If there is already a cookie with the same name in the collection, it will be removed first. - * @param Cookie $cookie the cookie to be added - * @throws InvalidCallException if the cookie collection is read only - */ - public function add($cookie) - { - if ($this->readOnly) { - throw new InvalidCallException('The cookie collection is read only.'); - } - $this->_cookies[$cookie->name] = $cookie; - } - - /** - * Removes a cookie. - * If `$removeFromBrowser` is true, the cookie will be removed from the browser. - * In this case, a cookie with outdated expiry will be added to the collection. - * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. - * @param boolean $removeFromBrowser whether to remove the cookie from browser - * @throws InvalidCallException if the cookie collection is read only - */ - public function remove($cookie, $removeFromBrowser = true) - { - if ($this->readOnly) { - throw new InvalidCallException('The cookie collection is read only.'); - } - if ($cookie instanceof Cookie) { - $cookie->expire = 1; - $cookie->value = ''; - } else { - $cookie = new Cookie([ - 'name' => $cookie, - 'expire' => 1, - ]); - } - if ($removeFromBrowser) { - $this->_cookies[$cookie->name] = $cookie; - } else { - unset($this->_cookies[$cookie->name]); - } - } - - /** - * Removes all cookies. - * @throws InvalidCallException if the cookie collection is read only - */ - public function removeAll() - { - if ($this->readOnly) { - throw new InvalidCallException('The cookie collection is read only.'); - } - $this->_cookies = []; - } - - /** - * Returns the collection as a PHP array. - * @return array the array representation of the collection. - * The array keys are cookie names, and the array values are the corresponding cookie objects. - */ - public function toArray() - { - return $this->_cookies; - } - - /** - * Returns whether there is a cookie with the specified name. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($collection[$name])`. - * @param string $name the cookie name - * @return boolean whether the named cookie exists - */ - public function offsetExists($name) - { - return $this->has($name); - } - - /** - * Returns the cookie with the specified name. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$cookie = $collection[$name];`. - * This is equivalent to [[get()]]. - * @param string $name the cookie name - * @return Cookie the cookie with the specified name, null if the named cookie does not exist. - */ - public function offsetGet($name) - { - return $this->get($name); - } - - /** - * Adds the cookie to the collection. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$collection[$name] = $cookie;`. - * This is equivalent to [[add()]]. - * @param string $name the cookie name - * @param Cookie $cookie the cookie to be added - */ - public function offsetSet($name, $cookie) - { - $this->add($cookie); - } - - /** - * Removes the named cookie. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($collection[$name])`. - * This is equivalent to [[remove()]]. - * @param string $name the cookie name - */ - public function offsetUnset($name) - { - $this->remove($name); - } + /** + * @var boolean whether this collection is read only. + */ + public $readOnly = false; + + /** + * @var Cookie[] the cookies in this collection (indexed by the cookie names) + */ + private $_cookies = []; + + /** + * Constructor. + * @param array $cookies the cookies that this collection initially contains. This should be + * an array of name-value pairs.s + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($cookies = [], $config = []) + { + $this->_cookies = $cookies; + parent::__construct($config); + } + + /** + * Returns an iterator for traversing the cookies in the collection. + * This method is required by the SPL interface `IteratorAggregate`. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return ArrayIterator an iterator for traversing the cookies in the collection. + */ + public function getIterator() + { + return new ArrayIterator($this->_cookies); + } + + /** + * Returns the number of cookies in the collection. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($collection)`. + * @return integer the number of cookies in the collection. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the number of cookies in the collection. + * @return integer the number of cookies in the collection. + */ + public function getCount() + { + return count($this->_cookies); + } + + /** + * Returns the cookie with the specified name. + * @param string $name the cookie name + * @return Cookie the cookie with the specified name. Null if the named cookie does not exist. + * @see getValue() + */ + public function get($name) + { + return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; + } + + /** + * Returns the value of the named cookie. + * @param string $name the cookie name + * @param mixed $defaultValue the value that should be returned when the named cookie does not exist. + * @return mixed the value of the named cookie. + * @see get() + */ + public function getValue($name, $defaultValue = null) + { + return isset($this->_cookies[$name]) ? $this->_cookies[$name]->value : $defaultValue; + } + + /** + * Returns whether there is a cookie with the specified name. + * @param string $name the cookie name + * @return boolean whether the named cookie exists + */ + public function has($name) + { + return isset($this->_cookies[$name]); + } + + /** + * Adds a cookie to the collection. + * If there is already a cookie with the same name in the collection, it will be removed first. + * @param Cookie $cookie the cookie to be added + * @throws InvalidCallException if the cookie collection is read only + */ + public function add($cookie) + { + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); + } + $this->_cookies[$cookie->name] = $cookie; + } + + /** + * Removes a cookie. + * If `$removeFromBrowser` is true, the cookie will be removed from the browser. + * In this case, a cookie with outdated expiry will be added to the collection. + * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. + * @param boolean $removeFromBrowser whether to remove the cookie from browser + * @throws InvalidCallException if the cookie collection is read only + */ + public function remove($cookie, $removeFromBrowser = true) + { + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); + } + if ($cookie instanceof Cookie) { + $cookie->expire = 1; + $cookie->value = ''; + } else { + $cookie = new Cookie([ + 'name' => $cookie, + 'expire' => 1, + ]); + } + if ($removeFromBrowser) { + $this->_cookies[$cookie->name] = $cookie; + } else { + unset($this->_cookies[$cookie->name]); + } + } + + /** + * Removes all cookies. + * @throws InvalidCallException if the cookie collection is read only + */ + public function removeAll() + { + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); + } + $this->_cookies = []; + } + + /** + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are cookie names, and the array values are the corresponding cookie objects. + */ + public function toArray() + { + return $this->_cookies; + } + + /** + * Returns whether there is a cookie with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the cookie name + * @return boolean whether the named cookie exists + */ + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the cookie with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$cookie = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the cookie name + * @return Cookie the cookie with the specified name, null if the named cookie does not exist. + */ + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Adds the cookie to the collection. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$collection[$name] = $cookie;`. + * This is equivalent to [[add()]]. + * @param string $name the cookie name + * @param Cookie $cookie the cookie to be added + */ + public function offsetSet($name, $cookie) + { + $this->add($cookie); + } + + /** + * Removes the named cookie. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the cookie name + */ + public function offsetUnset($name) + { + $this->remove($name); + } } diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index 2b0b0e716df..798f0e99488 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -36,189 +36,193 @@ */ class DbSession extends Session { - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbSession object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; - /** - * @var string the name of the DB table that stores the session data. - * The table should be pre-created as follows: - * - * ~~~ - * CREATE TABLE tbl_session - * ( - * id CHAR(40) NOT NULL PRIMARY KEY, - * expire INTEGER, - * data BLOB - * ) - * ~~~ - * - * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type - * that can be used for some popular DBMS: - * - * - MySQL: LONGBLOB - * - PostgreSQL: BYTEA - * - MSSQL: BLOB - * - * When using DbSession in a production server, we recommend you create a DB index for the 'expire' - * column in the session table to improve the performance. - */ - public $sessionTable = '{{%session}}'; - - /** - * Initializes the DbSession component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbSession::db must be either a DB connection instance or the application component ID of a DB connection."); - } - parent::init(); - } - - /** - * Returns a value indicating whether to use custom session storage. - * This method overrides the parent implementation and always returns true. - * @return boolean whether to use custom storage. - */ - public function getUseCustomStorage() - { - return true; - } - - /** - * Updates the current session ID with a newly generated one . - * Please refer to for more details. - * @param boolean $deleteOldSession Whether to delete the old associated session file or not. - */ - public function regenerateID($deleteOldSession = false) - { - $oldID = session_id(); - - // if no session is started, there is nothing to regenerate - if (empty($oldID)) { - return; - } - - parent::regenerateID(false); - $newID = session_id(); - - $query = new Query; - $row = $query->from($this->sessionTable) - ->where(['id' => $oldID]) - ->createCommand($this->db) - ->queryOne(); - if ($row !== false) { - if ($deleteOldSession) { - $this->db->createCommand() - ->update($this->sessionTable, ['id' => $newID], ['id' => $oldID]) - ->execute(); - } else { - $row['id'] = $newID; - $this->db->createCommand() - ->insert($this->sessionTable, $row) - ->execute(); - } - } else { - // shouldn't reach here normally - $this->db->createCommand() - ->insert($this->sessionTable, [ - 'id' => $newID, - 'expire' => time() + $this->getTimeout(), - ])->execute(); - } - } - - /** - * Session read handler. - * Do not call this method directly. - * @param string $id session ID - * @return string the session data - */ - public function readSession($id) - { - $query = new Query; - $data = $query->select(['data']) - ->from($this->sessionTable) - ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]) - ->createCommand($this->db) - ->queryScalar(); - return $data === false ? '' : $data; - } - - /** - * Session write handler. - * Do not call this method directly. - * @param string $id session ID - * @param string $data session data - * @return boolean whether session write is successful - */ - public function writeSession($id, $data) - { - // exception must be caught in session write handler - // http://us.php.net/manual/en/function.session-set-save-handler.php - try { - $expire = time() + $this->getTimeout(); - $query = new Query; - $exists = $query->select(['id']) - ->from($this->sessionTable) - ->where(['id' => $id]) - ->createCommand($this->db) - ->queryScalar(); - if ($exists === false) { - $this->db->createCommand() - ->insert($this->sessionTable, [ - 'id' => $id, - 'data' => $data, - 'expire' => $expire, - ])->execute(); - } else { - $this->db->createCommand() - ->update($this->sessionTable, ['data' => $data, 'expire' => $expire], ['id' => $id]) - ->execute(); - } - } catch (\Exception $e) { - if (YII_DEBUG) { - echo $e->getMessage(); - } - // it is too late to log an error message here - return false; - } - return true; - } - - /** - * Session destroy handler. - * Do not call this method directly. - * @param string $id session ID - * @return boolean whether session is destroyed successfully - */ - public function destroySession($id) - { - $this->db->createCommand() - ->delete($this->sessionTable, ['id' => $id]) - ->execute(); - return true; - } - - /** - * Session GC (garbage collection) handler. - * Do not call this method directly. - * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. - * @return boolean whether session is GCed successfully - */ - public function gcSession($maxLifetime) - { - $this->db->createCommand() - ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()]) - ->execute(); - return true; - } + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbSession object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the DB table that stores the session data. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_session + * ( + * id CHAR(40) NOT NULL PRIMARY KEY, + * expire INTEGER, + * data BLOB + * ) + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbSession in a production server, we recommend you create a DB index for the 'expire' + * column in the session table to improve the performance. + */ + public $sessionTable = '{{%session}}'; + + /** + * Initializes the DbSession component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbSession::db must be either a DB connection instance or the application component ID of a DB connection."); + } + parent::init(); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Updates the current session ID with a newly generated one . + * Please refer to for more details. + * @param boolean $deleteOldSession Whether to delete the old associated session file or not. + */ + public function regenerateID($deleteOldSession = false) + { + $oldID = session_id(); + + // if no session is started, there is nothing to regenerate + if (empty($oldID)) { + return; + } + + parent::regenerateID(false); + $newID = session_id(); + + $query = new Query; + $row = $query->from($this->sessionTable) + ->where(['id' => $oldID]) + ->createCommand($this->db) + ->queryOne(); + if ($row !== false) { + if ($deleteOldSession) { + $this->db->createCommand() + ->update($this->sessionTable, ['id' => $newID], ['id' => $oldID]) + ->execute(); + } else { + $row['id'] = $newID; + $this->db->createCommand() + ->insert($this->sessionTable, $row) + ->execute(); + } + } else { + // shouldn't reach here normally + $this->db->createCommand() + ->insert($this->sessionTable, [ + 'id' => $newID, + 'expire' => time() + $this->getTimeout(), + ])->execute(); + } + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $query = new Query; + $data = $query->select(['data']) + ->from($this->sessionTable) + ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]) + ->createCommand($this->db) + ->queryScalar(); + + return $data === false ? '' : $data; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + // exception must be caught in session write handler + // http://us.php.net/manual/en/function.session-set-save-handler.php + try { + $expire = time() + $this->getTimeout(); + $query = new Query; + $exists = $query->select(['id']) + ->from($this->sessionTable) + ->where(['id' => $id]) + ->createCommand($this->db) + ->queryScalar(); + if ($exists === false) { + $this->db->createCommand() + ->insert($this->sessionTable, [ + 'id' => $id, + 'data' => $data, + 'expire' => $expire, + ])->execute(); + } else { + $this->db->createCommand() + ->update($this->sessionTable, ['data' => $data, 'expire' => $expire], ['id' => $id]) + ->execute(); + } + } catch (\Exception $e) { + if (YII_DEBUG) { + echo $e->getMessage(); + } + // it is too late to log an error message here + return false; + } + + return true; + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + $this->db->createCommand() + ->delete($this->sessionTable, ['id' => $id]) + ->execute(); + + return true; + } + + /** + * Session GC (garbage collection) handler. + * Do not call this method directly. + * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. + * @return boolean whether session is GCed successfully + */ + public function gcSession($maxLifetime) + { + $this->db->createCommand() + ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()]) + ->execute(); + + return true; + } } diff --git a/framework/web/ErrorAction.php b/framework/web/ErrorAction.php index 95f17bee205..6910e97d690 100644 --- a/framework/web/ErrorAction.php +++ b/framework/web/ErrorAction.php @@ -49,58 +49,57 @@ */ class ErrorAction extends Action { - /** - * @var string the view file to be rendered. If not set, it will take the value of [[id]]. - * That means, if you name the action as "error" in "SiteController", then the view name - * would be "error", and the corresponding view file would be "views/site/error.php". - */ - public $view; - /** - * @var string the name of the error when the exception name cannot be determined. - * Defaults to "Error". - */ - public $defaultName; - /** - * @var string the message to be displayed when the exception message contains sensitive information. - * Defaults to "An internal server error occurred.". - */ - public $defaultMessage; + /** + * @var string the view file to be rendered. If not set, it will take the value of [[id]]. + * That means, if you name the action as "error" in "SiteController", then the view name + * would be "error", and the corresponding view file would be "views/site/error.php". + */ + public $view; + /** + * @var string the name of the error when the exception name cannot be determined. + * Defaults to "Error". + */ + public $defaultName; + /** + * @var string the message to be displayed when the exception message contains sensitive information. + * Defaults to "An internal server error occurred.". + */ + public $defaultMessage; + public function run() + { + if (($exception = Yii::$app->exception) === null) { + return ''; + } - public function run() - { - if (($exception = Yii::$app->exception) === null) { - return ''; - } + if ($exception instanceof HttpException) { + $code = $exception->statusCode; + } else { + $code = $exception->getCode(); + } + if ($exception instanceof Exception) { + $name = $exception->getName(); + } else { + $name = $this->defaultName ?: Yii::t('yii', 'Error'); + } + if ($code) { + $name .= " (#$code)"; + } - if ($exception instanceof HttpException) { - $code = $exception->statusCode; - } else { - $code = $exception->getCode(); - } - if ($exception instanceof Exception) { - $name = $exception->getName(); - } else { - $name = $this->defaultName ?: Yii::t('yii', 'Error'); - } - if ($code) { - $name .= " (#$code)"; - } + if ($exception instanceof UserException) { + $message = $exception->getMessage(); + } else { + $message = $this->defaultMessage ?: Yii::t('yii', 'An internal server error occurred.'); + } - if ($exception instanceof UserException) { - $message = $exception->getMessage(); - } else { - $message = $this->defaultMessage ?: Yii::t('yii', 'An internal server error occurred.'); - } - - if (Yii::$app->getRequest()->getIsAjax()) { - return "$name: $message"; - } else { - return $this->controller->render($this->view ?: $this->id, [ - 'name' => $name, - 'message' => $message, - 'exception' => $exception, - ]); - } - } + if (Yii::$app->getRequest()->getIsAjax()) { + return "$name: $message"; + } else { + return $this->controller->render($this->view ?: $this->id, [ + 'name' => $name, + 'message' => $message, + 'exception' => $exception, + ]); + } + } } diff --git a/framework/web/ForbiddenHttpException.php b/framework/web/ForbiddenHttpException.php index e96c2252933..f9388102c75 100644 --- a/framework/web/ForbiddenHttpException.php +++ b/framework/web/ForbiddenHttpException.php @@ -22,14 +22,14 @@ */ class ForbiddenHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(403, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(403, $message, $code, $previous); + } } diff --git a/framework/web/GoneHttpException.php b/framework/web/GoneHttpException.php index b78aa01b595..358a0f1fe72 100644 --- a/framework/web/GoneHttpException.php +++ b/framework/web/GoneHttpException.php @@ -21,14 +21,14 @@ */ class GoneHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(410, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(410, $message, $code, $previous); + } } diff --git a/framework/web/HeaderCollection.php b/framework/web/HeaderCollection.php index e8e4f9c4710..781eb939200 100644 --- a/framework/web/HeaderCollection.php +++ b/framework/web/HeaderCollection.php @@ -23,199 +23,204 @@ */ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable { - /** - * @var array the headers in this collection (indexed by the header names) - */ - private $_headers = []; - - /** - * Returns an iterator for traversing the headers in the collection. - * This method is required by the SPL interface `IteratorAggregate`. - * It will be implicitly called when you use `foreach` to traverse the collection. - * @return ArrayIterator an iterator for traversing the headers in the collection. - */ - public function getIterator() - { - return new ArrayIterator($this->_headers); - } - - /** - * Returns the number of headers in the collection. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($collection)`. - * @return integer the number of headers in the collection. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the number of headers in the collection. - * @return integer the number of headers in the collection. - */ - public function getCount() - { - return count($this->_headers); - } - - /** - * Returns the named header(s). - * @param string $name the name of the header to return - * @param mixed $default the value to return in case the named header does not exist - * @param boolean $first whether to only return the first header of the specified name. - * If false, all headers of the specified name will be returned. - * @return string|array the named header(s). If `$first` is true, a string will be returned; - * If `$first` is false, an array will be returned. - */ - public function get($name, $default = null, $first = true) - { - $name = strtolower($name); - if (isset($this->_headers[$name])) { - return $first ? reset($this->_headers[$name]) : $this->_headers[$name]; - } else { - return $default; - } - } - - /** - * Adds a new header. - * If there is already a header with the same name, it will be replaced. - * @param string $name the name of the header - * @param string $value the value of the header - * @return static the collection object itself - */ - public function set($name, $value = '') - { - $name = strtolower($name); - $this->_headers[$name] = (array)$value; - return $this; - } - - /** - * Adds a new header. - * If there is already a header with the same name, the new one will - * be appended to it instead of replacing it. - * @param string $name the name of the header - * @param string $value the value of the header - * @return static the collection object itself - */ - public function add($name, $value) - { - $name = strtolower($name); - $this->_headers[$name][] = $value; - return $this; - } - - /** - * Sets a new header only if it does not exist yet. - * If there is already a header with the same name, the new one will be ignored. - * @param string $name the name of the header - * @param string $value the value of the header - * @return static the collection object itself - */ - public function setDefault($name, $value) - { - $name = strtolower($name); - if (empty($this->_headers[$name])) { - $this->_headers[$name][] = $value; - } - return $this; - } - - /** - * Returns a value indicating whether the named header exists. - * @param string $name the name of the header - * @return boolean whether the named header exists - */ - public function has($name) - { - $name = strtolower($name); - return isset($this->_headers[$name]); - } - - /** - * Removes a header. - * @param string $name the name of the header to be removed. - * @return string the value of the removed header. Null is returned if the header does not exist. - */ - public function remove($name) - { - $name = strtolower($name); - if (isset($this->_headers[$name])) { - $value = $this->_headers[$name]; - unset($this->_headers[$name]); - return $value; - } else { - return null; - } - } - - /** - * Removes all headers. - */ - public function removeAll() - { - $this->_headers = []; - } - - /** - * Returns the collection as a PHP array. - * @return array the array representation of the collection. - * The array keys are header names, and the array values are the corresponding header values. - */ - public function toArray() - { - return $this->_headers; - } - - /** - * Returns whether there is a header with the specified name. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `isset($collection[$name])`. - * @param string $name the header name - * @return boolean whether the named header exists - */ - public function offsetExists($name) - { - return $this->has($name); - } - - /** - * Returns the header with the specified name. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$header = $collection[$name];`. - * This is equivalent to [[get()]]. - * @param string $name the header name - * @return string the header value with the specified name, null if the named header does not exist. - */ - public function offsetGet($name) - { - return $this->get($name); - } - - /** - * Adds the header to the collection. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `$collection[$name] = $header;`. - * This is equivalent to [[add()]]. - * @param string $name the header name - * @param string $value the header value to be added - */ - public function offsetSet($name, $value) - { - $this->set($name, $value); - } - - /** - * Removes the named header. - * This method is required by the SPL interface `ArrayAccess`. - * It is implicitly called when you use something like `unset($collection[$name])`. - * This is equivalent to [[remove()]]. - * @param string $name the header name - */ - public function offsetUnset($name) - { - $this->remove($name); - } + /** + * @var array the headers in this collection (indexed by the header names) + */ + private $_headers = []; + + /** + * Returns an iterator for traversing the headers in the collection. + * This method is required by the SPL interface `IteratorAggregate`. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return ArrayIterator an iterator for traversing the headers in the collection. + */ + public function getIterator() + { + return new ArrayIterator($this->_headers); + } + + /** + * Returns the number of headers in the collection. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($collection)`. + * @return integer the number of headers in the collection. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the number of headers in the collection. + * @return integer the number of headers in the collection. + */ + public function getCount() + { + return count($this->_headers); + } + + /** + * Returns the named header(s). + * @param string $name the name of the header to return + * @param mixed $default the value to return in case the named header does not exist + * @param boolean $first whether to only return the first header of the specified name. + * If false, all headers of the specified name will be returned. + * @return string|array the named header(s). If `$first` is true, a string will be returned; + * If `$first` is false, an array will be returned. + */ + public function get($name, $default = null, $first = true) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + return $first ? reset($this->_headers[$name]) : $this->_headers[$name]; + } else { + return $default; + } + } + + /** + * Adds a new header. + * If there is already a header with the same name, it will be replaced. + * @param string $name the name of the header + * @param string $value the value of the header + * @return static the collection object itself + */ + public function set($name, $value = '') + { + $name = strtolower($name); + $this->_headers[$name] = (array) $value; + + return $this; + } + + /** + * Adds a new header. + * If there is already a header with the same name, the new one will + * be appended to it instead of replacing it. + * @param string $name the name of the header + * @param string $value the value of the header + * @return static the collection object itself + */ + public function add($name, $value) + { + $name = strtolower($name); + $this->_headers[$name][] = $value; + + return $this; + } + + /** + * Sets a new header only if it does not exist yet. + * If there is already a header with the same name, the new one will be ignored. + * @param string $name the name of the header + * @param string $value the value of the header + * @return static the collection object itself + */ + public function setDefault($name, $value) + { + $name = strtolower($name); + if (empty($this->_headers[$name])) { + $this->_headers[$name][] = $value; + } + + return $this; + } + + /** + * Returns a value indicating whether the named header exists. + * @param string $name the name of the header + * @return boolean whether the named header exists + */ + public function has($name) + { + $name = strtolower($name); + + return isset($this->_headers[$name]); + } + + /** + * Removes a header. + * @param string $name the name of the header to be removed. + * @return string the value of the removed header. Null is returned if the header does not exist. + */ + public function remove($name) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + $value = $this->_headers[$name]; + unset($this->_headers[$name]); + + return $value; + } else { + return null; + } + } + + /** + * Removes all headers. + */ + public function removeAll() + { + $this->_headers = []; + } + + /** + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are header names, and the array values are the corresponding header values. + */ + public function toArray() + { + return $this->_headers; + } + + /** + * Returns whether there is a header with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the header name + * @return boolean whether the named header exists + */ + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the header with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$header = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the header name + * @return string the header value with the specified name, null if the named header does not exist. + */ + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Adds the header to the collection. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$collection[$name] = $header;`. + * This is equivalent to [[add()]]. + * @param string $name the header name + * @param string $value the header value to be added + */ + public function offsetSet($name, $value) + { + $this->set($name, $value); + } + + /** + * Removes the named header. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the header name + */ + public function offsetUnset($name) + { + $this->remove($name); + } } diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php index fd579981cff..a319ebb1692 100644 --- a/framework/web/HttpCache.php +++ b/framework/web/HttpCache.php @@ -45,116 +45,118 @@ */ class HttpCache extends ActionFilter { - /** - * @var callable a PHP callback that returns the UNIX timestamp of the last modification time. - * The callback's signature should be: - * - * ~~~ - * function ($action, $params) - * ~~~ - * - * where `$action` is the [[Action]] object that this filter is currently handling; - * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp. - */ - public $lastModified; - /** - * @var callable a PHP callback that generates the Etag seed string. - * The callback's signature should be: - * - * ~~~ - * function ($action, $params) - * ~~~ - * - * where `$action` is the [[Action]] object that this filter is currently handling; - * `$params` takes the value of [[params]]. The callback should return a string serving - * as the seed for generating an Etag. - */ - public $etagSeed; - /** - * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. - */ - public $params; - /** - * @var string HTTP cache control header. If null, the header will not be sent. - */ - public $cacheControlHeader = 'max-age=3600, public'; + /** + * @var callable a PHP callback that returns the UNIX timestamp of the last modification time. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp. + */ + public $lastModified; + /** + * @var callable a PHP callback that generates the Etag seed string. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a string serving + * as the seed for generating an Etag. + */ + public $etagSeed; + /** + * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. + */ + public $params; + /** + * @var string HTTP cache control header. If null, the header will not be sent. + */ + public $cacheControlHeader = 'max-age=3600, public'; - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $verb = Yii::$app->getRequest()->getMethod(); - if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { - return true; - } + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $verb = Yii::$app->getRequest()->getMethod(); + if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { + return true; + } - $lastModified = $etag = null; - if ($this->lastModified !== null) { - $lastModified = call_user_func($this->lastModified, $action, $this->params); - } - if ($this->etagSeed !== null) { - $seed = call_user_func($this->etagSeed, $action, $this->params); - $etag = $this->generateEtag($seed); - } + $lastModified = $etag = null; + if ($this->lastModified !== null) { + $lastModified = call_user_func($this->lastModified, $action, $this->params); + } + if ($this->etagSeed !== null) { + $seed = call_user_func($this->etagSeed, $action, $this->params); + $etag = $this->generateEtag($seed); + } - $this->sendCacheControlHeader(); - $response = Yii::$app->getResponse(); - if ($etag !== null) { - $response->getHeaders()->set('Etag', $etag); - } + $this->sendCacheControlHeader(); + $response = Yii::$app->getResponse(); + if ($etag !== null) { + $response->getHeaders()->set('Etag', $etag); + } - if ($this->validateCache($lastModified, $etag)) { - $response->setStatusCode(304); - return false; - } + if ($this->validateCache($lastModified, $etag)) { + $response->setStatusCode(304); - if ($lastModified !== null) { - $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); - } - return true; - } + return false; + } - /** - * Validates if the HTTP cache contains valid content. - * @param integer $lastModified the calculated Last-Modified value in terms of a UNIX timestamp. - * If null, the Last-Modified header will not be validated. - * @param string $etag the calculated ETag value. If null, the ETag header will not be validated. - * @return boolean whether the HTTP cache is still valid. - */ - protected function validateCache($lastModified, $etag) - { - if ($lastModified !== null && (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified)) { - return false; - } else { - return $etag === null || isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag; - } - } + if ($lastModified !== null) { + $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + } - /** - * Sends the cache control header to the client - * @see cacheControl - */ - protected function sendCacheControlHeader() - { - session_cache_limiter('public'); - $headers = Yii::$app->getResponse()->getHeaders(); - $headers->set('Pragma'); - if ($this->cacheControlHeader !== null) { - $headers->set('Cache-Control', $this->cacheControlHeader); - } - } + return true; + } - /** - * Generates an Etag from the given seed string. - * @param string $seed Seed for the ETag - * @return string the generated Etag - */ - protected function generateEtag($seed) - { - return '"' . base64_encode(sha1($seed, true)) . '"'; - } + /** + * Validates if the HTTP cache contains valid content. + * @param integer $lastModified the calculated Last-Modified value in terms of a UNIX timestamp. + * If null, the Last-Modified header will not be validated. + * @param string $etag the calculated ETag value. If null, the ETag header will not be validated. + * @return boolean whether the HTTP cache is still valid. + */ + protected function validateCache($lastModified, $etag) + { + if ($lastModified !== null && (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified)) { + return false; + } else { + return $etag === null || isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag; + } + } + + /** + * Sends the cache control header to the client + * @see cacheControl + */ + protected function sendCacheControlHeader() + { + session_cache_limiter('public'); + $headers = Yii::$app->getResponse()->getHeaders(); + $headers->set('Pragma'); + if ($this->cacheControlHeader !== null) { + $headers->set('Cache-Control', $this->cacheControlHeader); + } + } + + /** + * Generates an Etag from the given seed string. + * @param string $seed Seed for the ETag + * @return string the generated Etag + */ + protected function generateEtag($seed) + { + return '"' . base64_encode(sha1($seed, true)) . '"'; + } } diff --git a/framework/web/HttpException.php b/framework/web/HttpException.php index 630419d44e0..a1dffd68b90 100644 --- a/framework/web/HttpException.php +++ b/framework/web/HttpException.php @@ -29,33 +29,33 @@ */ class HttpException extends UserException { - /** - * @var integer HTTP status code, such as 403, 404, 500, etc. - */ - public $statusCode; + /** + * @var integer HTTP status code, such as 403, 404, 500, etc. + */ + public $statusCode; - /** - * Constructor. - * @param integer $status HTTP status code, such as 404, 500, etc. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($status, $message = null, $code = 0, \Exception $previous = null) - { - $this->statusCode = $status; - parent::__construct($message, $code, $previous); - } + /** + * Constructor. + * @param integer $status HTTP status code, such as 404, 500, etc. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($status, $message = null, $code = 0, \Exception $previous = null) + { + $this->statusCode = $status; + parent::__construct($message, $code, $previous); + } - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - if (isset(Response::$httpStatuses[$this->statusCode])) { - return Response::$httpStatuses[$this->statusCode]; - } else { - return 'Error'; - } - } + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + if (isset(Response::$httpStatuses[$this->statusCode])) { + return Response::$httpStatuses[$this->statusCode]; + } else { + return 'Error'; + } + } } diff --git a/framework/web/IdentityInterface.php b/framework/web/IdentityInterface.php index 2aac17f1fc7..99e18fe6e18 100644 --- a/framework/web/IdentityInterface.php +++ b/framework/web/IdentityInterface.php @@ -43,47 +43,47 @@ */ interface IdentityInterface { - /** - * Finds an identity by the given ID. - * @param string|integer $id the ID to be looked for - * @return IdentityInterface the identity object that matches the given ID. - * Null should be returned if such an identity cannot be found - * or the identity is not in an active state (disabled, deleted, etc.) - */ - public static function findIdentity($id); - /** - * Finds an identity by the given secrete token. - * @param string $token the secrete token - * @return IdentityInterface the identity object that matches the given token. - * Null should be returned if such an identity cannot be found - * or the identity is not in an active state (disabled, deleted, etc.) - */ - public static function findIdentityByAccessToken($token); - /** - * Returns an ID that can uniquely identify a user identity. - * @return string|integer an ID that uniquely identifies a user identity. - */ - public function getId(); - /** - * Returns a key that can be used to check the validity of a given identity ID. - * - * The key should be unique for each individual user, and should be persistent - * so that it can be used to check the validity of the user identity. - * - * The space of such keys should be big enough to defeat potential identity attacks. - * - * This is required if [[User::enableAutoLogin]] is enabled. - * @return string a key that is used to check the validity of a given identity ID. - * @see validateAuthKey() - */ - public function getAuthKey(); - /** - * Validates the given auth key. - * - * This is required if [[User::enableAutoLogin]] is enabled. - * @param string $authKey the given auth key - * @return boolean whether the given auth key is valid. - * @see getAuthKey() - */ - public function validateAuthKey($authKey); + /** + * Finds an identity by the given ID. + * @param string|integer $id the ID to be looked for + * @return IdentityInterface the identity object that matches the given ID. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) + */ + public static function findIdentity($id); + /** + * Finds an identity by the given secrete token. + * @param string $token the secrete token + * @return IdentityInterface the identity object that matches the given token. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) + */ + public static function findIdentityByAccessToken($token); + /** + * Returns an ID that can uniquely identify a user identity. + * @return string|integer an ID that uniquely identifies a user identity. + */ + public function getId(); + /** + * Returns a key that can be used to check the validity of a given identity ID. + * + * The key should be unique for each individual user, and should be persistent + * so that it can be used to check the validity of the user identity. + * + * The space of such keys should be big enough to defeat potential identity attacks. + * + * This is required if [[User::enableAutoLogin]] is enabled. + * @return string a key that is used to check the validity of a given identity ID. + * @see validateAuthKey() + */ + public function getAuthKey(); + /** + * Validates the given auth key. + * + * This is required if [[User::enableAutoLogin]] is enabled. + * @param string $authKey the given auth key + * @return boolean whether the given auth key is valid. + * @see getAuthKey() + */ + public function validateAuthKey($authKey); } diff --git a/framework/web/JqueryAsset.php b/framework/web/JqueryAsset.php index 90d2df6b69b..96cbf032b44 100644 --- a/framework/web/JqueryAsset.php +++ b/framework/web/JqueryAsset.php @@ -15,8 +15,8 @@ */ class JqueryAsset extends AssetBundle { - public $sourcePath = '@vendor/yiisoft/jquery'; - public $js = [ - 'jquery.js', - ]; + public $sourcePath = '@vendor/yiisoft/jquery'; + public $js = [ + 'jquery.js', + ]; } diff --git a/framework/web/JsExpression.php b/framework/web/JsExpression.php index eedd277a1a9..8c5db39c362 100644 --- a/framework/web/JsExpression.php +++ b/framework/web/JsExpression.php @@ -20,28 +20,28 @@ */ class JsExpression extends Object { - /** - * @var string the JavaScript expression represented by this object - */ - public $expression; + /** + * @var string the JavaScript expression represented by this object + */ + public $expression; - /** - * Constructor. - * @param string $expression the JavaScript expression represented by this object - * @param array $config additional configurations for this object - */ - public function __construct($expression, $config = []) - { - $this->expression = $expression; - parent::__construct($config); - } + /** + * Constructor. + * @param string $expression the JavaScript expression represented by this object + * @param array $config additional configurations for this object + */ + public function __construct($expression, $config = []) + { + $this->expression = $expression; + parent::__construct($config); + } - /** - * The PHP magic function converting an object into a string. - * @return string the JavaScript expression. - */ - public function __toString() - { - return $this->expression; - } + /** + * The PHP magic function converting an object into a string. + * @return string the JavaScript expression. + */ + public function __toString() + { + return $this->expression; + } } diff --git a/framework/web/JsonParser.php b/framework/web/JsonParser.php index 61dec1b1eae..a7e00446c3d 100644 --- a/framework/web/JsonParser.php +++ b/framework/web/JsonParser.php @@ -18,32 +18,32 @@ */ class JsonParser implements RequestParserInterface { - /** - * @var boolean whether to return objects in terms of associative arrays. - */ - public $asArray = true; - /** - * @var boolean whether to throw a [[BadRequestHttpException]] if the body is invalid json - */ - public $throwException = true; + /** + * @var boolean whether to return objects in terms of associative arrays. + */ + public $asArray = true; + /** + * @var boolean whether to throw a [[BadRequestHttpException]] if the body is invalid json + */ + public $throwException = true; + /** + * Parses a HTTP request body. + * @param string $rawBody the raw HTTP request body. + * @param string $contentType the content type specified for the request body. + * @return array parameters parsed from the request body + * @throws BadRequestHttpException if the body contains invalid json and [[throwException]] is `true`. + */ + public function parse($rawBody, $contentType) + { + try { + return Json::decode($rawBody, $this->asArray); + } catch (InvalidParamException $e) { + if ($this->throwException) { + throw new BadRequestHttpException('Invalid JSON data in request body: ' . $e->getMessage(), 0, $e); + } - /** - * Parses a HTTP request body. - * @param string $rawBody the raw HTTP request body. - * @param string $contentType the content type specified for the request body. - * @return array parameters parsed from the request body - * @throws BadRequestHttpException if the body contains invalid json and [[throwException]] is `true`. - */ - public function parse($rawBody, $contentType) - { - try { - return Json::decode($rawBody, $this->asArray); - } catch (InvalidParamException $e) { - if ($this->throwException) { - throw new BadRequestHttpException('Invalid JSON data in request body: ' . $e->getMessage(), 0, $e); - } - return null; - } - } + return null; + } + } } diff --git a/framework/web/Link.php b/framework/web/Link.php index a2a4057e38d..e872deae13b 100644 --- a/framework/web/Link.php +++ b/framework/web/Link.php @@ -17,59 +17,59 @@ */ class Link extends Object { - /** - * The self link. - */ - const REL_SELF = 'self'; + /** + * The self link. + */ + const REL_SELF = 'self'; - /** - * @var string a URI [RFC3986](https://tools.ietf.org/html/rfc3986) or - * URI template [RFC6570](https://tools.ietf.org/html/rfc6570). This property is required. - */ - public $href; - /** - * @var string a secondary key for selecting Link Objects which share the same relation type - */ - public $name; - /** - * @var string a hint to indicate the media type expected when dereferencing the target resource - */ - public $type; - /** - * @var boolean a value indicating whether [[href]] refers to a URI or URI template. - */ - public $templated = false; - /** - * @var string a URI that hints about the profile of the target resource. - */ - public $profile; - /** - * @var string a label describing the link - */ - public $title; - /** - * @var string the language of the target resource - */ - public $hreflang; + /** + * @var string a URI [RFC3986](https://tools.ietf.org/html/rfc3986) or + * URI template [RFC6570](https://tools.ietf.org/html/rfc6570). This property is required. + */ + public $href; + /** + * @var string a secondary key for selecting Link Objects which share the same relation type + */ + public $name; + /** + * @var string a hint to indicate the media type expected when dereferencing the target resource + */ + public $type; + /** + * @var boolean a value indicating whether [[href]] refers to a URI or URI template. + */ + public $templated = false; + /** + * @var string a URI that hints about the profile of the target resource. + */ + public $profile; + /** + * @var string a label describing the link + */ + public $title; + /** + * @var string the language of the target resource + */ + public $hreflang; + /** + * Serializes a list of links into proper array format. + * @param array $links the links to be serialized + * @return array the proper array representation of the links. + */ + public static function serialize(array $links) + { + foreach ($links as $rel => $link) { + if (is_array($link)) { + foreach ($link as $i => $l) { + $link[$i] = $l instanceof self ? array_filter((array) $l) : ['href' => $l]; + } + $links[$rel] = $link; + } elseif (!$link instanceof self) { + $links[$rel] = ['href' => $link]; + } + } - /** - * Serializes a list of links into proper array format. - * @param array $links the links to be serialized - * @return array the proper array representation of the links. - */ - public static function serialize(array $links) - { - foreach ($links as $rel => $link) { - if (is_array($link)) { - foreach ($link as $i => $l) { - $link[$i] = $l instanceof self ? array_filter((array)$l) : ['href' => $l]; - } - $links[$rel] = $link; - } elseif (!$link instanceof self) { - $links[$rel] = ['href' => $link]; - } - } - return $links; - } + return $links; + } } diff --git a/framework/web/Linkable.php b/framework/web/Linkable.php index 8d1558b05ff..faa82cfacb2 100644 --- a/framework/web/Linkable.php +++ b/framework/web/Linkable.php @@ -15,28 +15,28 @@ */ interface Linkable { - /** - * Returns a list of links. - * - * Each link is either a URI or a [[Link]] object. The return value of this method should - * be an array whose keys are the relation names and values the corresponding links. - * - * If a relation name corresponds to multiple links, use an array to represent them. - * - * For example, - * - * ```php - * [ - * 'self' => 'http://example.com/users/1', - * 'friends' => [ - * 'http://example.com/users/2', - * 'http://example.com/users/3', - * ], - * 'manager' => $managerLink, // $managerLink is a Link object - * ] - * ``` - * - * @return array the links - */ - public function getLinks(); + /** + * Returns a list of links. + * + * Each link is either a URI or a [[Link]] object. The return value of this method should + * be an array whose keys are the relation names and values the corresponding links. + * + * If a relation name corresponds to multiple links, use an array to represent them. + * + * For example, + * + * ```php + * [ + * 'self' => 'http://example.com/users/1', + * 'friends' => [ + * 'http://example.com/users/2', + * 'http://example.com/users/3', + * ], + * 'manager' => $managerLink, // $managerLink is a Link object + * ] + * ``` + * + * @return array the links + */ + public function getLinks(); } diff --git a/framework/web/MethodNotAllowedHttpException.php b/framework/web/MethodNotAllowedHttpException.php index d894f57b87d..50d9120cb97 100644 --- a/framework/web/MethodNotAllowedHttpException.php +++ b/framework/web/MethodNotAllowedHttpException.php @@ -15,14 +15,14 @@ */ class MethodNotAllowedHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(405, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(405, $message, $code, $previous); + } } diff --git a/framework/web/NotAcceptableHttpException.php b/framework/web/NotAcceptableHttpException.php index 5a749c91e85..8c76206b333 100644 --- a/framework/web/NotAcceptableHttpException.php +++ b/framework/web/NotAcceptableHttpException.php @@ -20,14 +20,14 @@ */ class NotAcceptableHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(406, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(406, $message, $code, $previous); + } } diff --git a/framework/web/NotFoundHttpException.php b/framework/web/NotFoundHttpException.php index 71f246d8460..ffdc546fcd9 100644 --- a/framework/web/NotFoundHttpException.php +++ b/framework/web/NotFoundHttpException.php @@ -15,14 +15,14 @@ */ class NotFoundHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(404, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(404, $message, $code, $previous); + } } diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php index 08857d530ba..15ee95fdc82 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -49,99 +49,100 @@ */ class PageCache extends ActionFilter { - /** - * @var boolean whether the content being cached should be differentiated according to the route. - * A route consists of the requested controller ID and action ID. Defaults to true. - */ - public $varyByRoute = true; - /** - * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. - */ - public $cache = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * [ - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ] - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * [ - * Yii::$app->language, - * ] - * ~~~ - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - /** - * @var \yii\base\View the view component to use for caching. If not set, the default application view component - * [[Application::view]] will be used. - */ - public $view; + /** + * @var boolean whether the content being cached should be differentiated according to the route. + * A route consists of the requested controller ID and action ID. Defaults to true. + */ + public $varyByRoute = true; + /** + * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * [ + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ] + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * [ + * Yii::$app->language, + * ] + * ~~~ + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var \yii\base\View the view component to use for caching. If not set, the default application view component + * [[Application::view]] will be used. + */ + public $view; + public function init() + { + parent::init(); + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + } - public function init() - { - parent::init(); - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - } + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $properties = []; + foreach (['cache', 'duration', 'dependency', 'variations', 'enabled'] as $name) { + $properties[$name] = $this->$name; + } + $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; + ob_start(); + ob_implicit_flush(false); + if ($this->view->beginCache($id, $properties)) { + return true; + } else { + Yii::$app->getResponse()->content = ob_get_clean(); - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $properties = []; - foreach (['cache', 'duration', 'dependency', 'variations', 'enabled'] as $name) { - $properties[$name] = $this->$name; - } - $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; - ob_start(); - ob_implicit_flush(false); - if ($this->view->beginCache($id, $properties)) { - return true; - } else { - Yii::$app->getResponse()->content = ob_get_clean(); - return false; - } - } + return false; + } + } - /** - * @inheritdoc - */ - public function afterAction($action, $result) - { - echo $result; - $this->view->endCache(); - return ob_get_clean(); - } + /** + * @inheritdoc + */ + public function afterAction($action, $result) + { + echo $result; + $this->view->endCache(); + + return ob_get_clean(); + } } diff --git a/framework/web/Request.php b/framework/web/Request.php index e772ba23468..0d00059f486 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -83,1229 +83,1255 @@ */ class Request extends \yii\base\Request { - /** - * The name of the HTTP header for sending CSRF token. - */ - const CSRF_HEADER = 'X-CSRF-Token'; - /** - * The length of the CSRF token mask. - */ - const CSRF_MASK_LENGTH = 8; - - - /** - * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to true. - * When CSRF validation is enabled, forms submitted to an Yii Web application must be originated - * from the same application. If not, a 400 HTTP exception will be raised. - * - * Note, this feature requires that the user client accepts cookie. Also, to use this feature, - * forms submitted via POST method must contain a hidden input whose name is specified by [[csrfParam]]. - * You may use [[\yii\web\Html::beginForm()]] to generate his hidden input. - * - * In JavaScript, you may get the values of [[csrfParam]] and [[csrfToken]] via `yii.getCsrfParam()` and - * `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered. - * - * @see Controller::enableCsrfValidation - * @see http://en.wikipedia.org/wiki/Cross-site_request_forgery - */ - public $enableCsrfValidation = true; - /** - * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'. - * This property is used only when [[enableCsrfValidation]] is true. - */ - public $csrfParam = '_csrf'; - /** - * @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. - * @see Cookie - */ - public $csrfCookie = ['httpOnly' => true]; - /** - * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. - */ - public $enableCookieValidation = true; - /** - * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT, PATCH or DELETE - * request tunneled through POST. Default to '_method'. - * @see getMethod() - * @see getBodyParams() - */ - public $methodParam = '_method'; - /** - * @var array the parsers for converting the raw HTTP request body into [[bodyParams]]. - * The array keys are the request `Content-Types`, and the array values are the - * corresponding configurations for [[Yii::createObject|creating the parser objects]]. - * A parser must implement the [[RequestParserInterface]]. - * - * To enable parsing for JSON requests you can use the [[JsonParser]] class like in the following example: - * - * ``` - * [ - * 'application/json' => 'yii\web\JsonParser', - * ] - * ``` - * - * To register a parser for parsing all request types you can use `'*'` as the array key. - * This one will be used as a fallback in case no other types match. - * - * @see getBodyParams() - */ - public $parsers = []; - - /** - * @var CookieCollection Collection of request cookies. - */ - private $_cookies; - /** - * @var array the headers in this collection (indexed by the header names) - */ - private $_headers; - - - /** - * Resolves the current request into a route and the associated parameters. - * @return array the first element is the route, and the second is the associated parameters. - * @throws HttpException if the request cannot be resolved. - */ - public function resolve() - { - $result = Yii::$app->getUrlManager()->parseRequest($this); - if ($result !== false) { - list ($route, $params) = $result; - $_GET = array_merge($_GET, $params); - return [$route, $_GET]; - } else { - throw new NotFoundHttpException(Yii::t('yii', 'Page not found.')); - } - } - - /** - * Returns the header collection. - * The header collection contains incoming HTTP headers. - * @return HeaderCollection the header collection - */ - public function getHeaders() - { - if ($this->_headers === null) { - $this->_headers = new HeaderCollection; - if (function_exists('getallheaders')) { - $headers = getallheaders(); - } elseif (function_exists('http_get_request_headers')) { - $headers = http_get_request_headers(); - } else { - foreach ($_SERVER as $name => $value) { - if (strncmp($name, 'HTTP_', 5) === 0) { - $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); - $this->_headers->add($name, $value); - } - } - return $this->_headers; - } - foreach ($headers as $name => $value) { - $this->_headers->add($name, $value); - } - } - - return $this->_headers; - } - - /** - * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, PATCH, DELETE). - * @return string request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. - * The value returned is turned into upper case. - */ - public function getMethod() - { - if (isset($_POST[$this->methodParam])) { - return strtoupper($_POST[$this->methodParam]); - } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { - return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); - } else { - return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; - } - } - - /** - * Returns whether this is a GET request. - * @return boolean whether this is a GET request. - */ - public function getIsGet() - { - return $this->getMethod() === 'GET'; - } - - /** - * Returns whether this is an OPTIONS request. - * @return boolean whether this is a OPTIONS request. - */ - public function getIsOptions() - { - return $this->getMethod() === 'OPTIONS'; - } - - /** - * Returns whether this is a HEAD request. - * @return boolean whether this is a HEAD request. - */ - public function getIsHead() - { - return $this->getMethod() === 'HEAD'; - } - - /** - * Returns whether this is a POST request. - * @return boolean whether this is a POST request. - */ - public function getIsPost() - { - return $this->getMethod() === 'POST'; - } - - /** - * Returns whether this is a DELETE request. - * @return boolean whether this is a DELETE request. - */ - public function getIsDelete() - { - return $this->getMethod() === 'DELETE'; - } - - /** - * Returns whether this is a PUT request. - * @return boolean whether this is a PUT request. - */ - public function getIsPut() - { - return $this->getMethod() === 'PUT'; - } - - /** - * Returns whether this is a PATCH request. - * @return boolean whether this is a PATCH request. - */ - public function getIsPatch() - { - return $this->getMethod() === 'PATCH'; - } - - /** - * Returns whether this is an AJAX (XMLHttpRequest) request. - * @return boolean whether this is an AJAX (XMLHttpRequest) request. - */ - public function getIsAjax() - { - return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; - } - - /** - * Returns whether this is a PJAX request - * @return boolean whether this is a PJAX request - */ - public function getIsPjax () - { - return $this->getIsAjax() && !empty($_SERVER['HTTP_X_PJAX']); - } - - /** - * Returns whether this is an Adobe Flash or Flex request. - * @return boolean whether this is an Adobe Flash or Adobe Flex request. - */ - public function getIsFlash() - { - return isset($_SERVER['HTTP_USER_AGENT']) && - (stripos($_SERVER['HTTP_USER_AGENT'], 'Shockwave') !== false || stripos($_SERVER['HTTP_USER_AGENT'], 'Flash') !== false); - } - - private $_rawBody; - - /** - * Returns the raw HTTP request body. - * @return string the request body - */ - public function getRawBody() - { - if ($this->_rawBody === null) { - $this->_rawBody = file_get_contents('php://input'); - } - return $this->_rawBody; - } - - private $_bodyParams; - - /** - * Returns the request parameters given in the request body. - * - * Request parameters are determined using the parsers configured in [[parsers]] property. - * If no parsers are configured for the current [[contentType]] it uses the PHP function [[mb_parse_str()]] - * to parse the [[rawBody|request body]]. - * @return array the request parameters given in the request body. - * @throws \yii\base\InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]]. - * @see getMethod() - * @see getBodyParam() - * @see setBodyParams() - */ - public function getBodyParams() - { - if ($this->_bodyParams === null) { - $contentType = $this->getContentType(); - if (isset($_POST[$this->methodParam])) { - $this->_bodyParams = $_POST; - unset($this->_bodyParams[$this->methodParam]); - } elseif (isset($this->parsers[$contentType])) { - $parser = Yii::createObject($this->parsers[$contentType]); - if (!($parser instanceof RequestParserInterface)) { - throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface."); - } - $this->_bodyParams = $parser->parse($this->getRawBody(), $contentType); - } elseif (isset($this->parsers['*'])) { - $parser = Yii::createObject($this->parsers['*']); - if (!($parser instanceof RequestParserInterface)) { - throw new InvalidConfigException("The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface."); - } - $this->_bodyParams = $parser->parse($this->getRawBody(), $contentType); - } elseif ($this->getMethod() === 'POST') { - // PHP has already parsed the body so we have all params in $_POST - $this->_bodyParams = $_POST; - } else { - $this->_bodyParams = []; - mb_parse_str($this->getRawBody(), $this->_bodyParams); - } - } - return $this->_bodyParams; - } - - /** - * Sets the request body parameters. - * @param array $values the request body parameters (name-value pairs) - * @see getBodyParam() - * @see getBodyParams() - */ - public function setBodyParams($values) - { - $this->_bodyParams = $values; - } - - /** - * Returns the named request body parameter value. - * @param string $name the parameter name - * @param mixed $defaultValue the default parameter value if the parameter does not exist. - * @return mixed the parameter value - * @see getBodyParams() - * @see setBodyParams() - */ - public function getBodyParam($name, $defaultValue = null) - { - $params = $this->getBodyParams(); - return isset($params[$name]) ? $params[$name] : $defaultValue; - } - - /** - * Returns POST parameter with a given name. If name isn't specified, returns an array of all POST parameters. - * - * @param string $name the parameter name - * @param mixed $defaultValue the default parameter value if the parameter does not exist. - * @return array|mixed - */ - public function post($name = null, $defaultValue = null) - { - if ($name === null) { - return $this->getBodyParams(); - } else { - return $this->getBodyParam($name, $defaultValue); - } - } - - private $_queryParams; - - /** - * Returns the request parameters given in the [[queryString]]. - * - * This method will return the contents of `$_GET` if params where not explicitly set. - * @return array the request GET parameter values. - * @see setQueryParams() - */ - public function getQueryParams() - { - if ($this->_queryParams === null) { - return $_GET; - } - return $this->_queryParams; - } - - /** - * Sets the request [[queryString]] parameters. - * @param array $values the request query parameters (name-value pairs) - * @see getQueryParam() - * @see getQueryParams() - */ - public function setQueryParams($values) - { - $this->_queryParams = $values; - } - - /** - * Returns GET parameter with a given name. If name isn't specified, returns an array of all GET parameters. - * - * @param string $name the parameter name - * @param mixed $defaultValue the default parameter value if the parameter does not exist. - * @return array|mixed - */ - public function get($name = null, $defaultValue = null) - { - if ($name === null) { - return $this->getQueryParams(); - } else { - return $this->getQueryParam($name, $defaultValue); - } - } - - /** - * Returns the named GET parameter value. - * If the GET parameter does not exist, the second parameter to this method will be returned. - * @param string $name the GET parameter name. If not specified, whole $_GET is returned. - * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. - * @return mixed the GET parameter value - * @see getBodyParam() - */ - public function getQueryParam($name, $defaultValue = null) - { - $params = $this->getQueryParams(); - return isset($params[$name]) ? $params[$name] : $defaultValue; - } - - private $_hostInfo; - - /** - * Returns the schema and host part of the current request URL. - * The returned URL does not have an ending slash. - * By default this is determined based on the user request information. - * You may explicitly specify it by setting the [[setHostInfo()|hostInfo]] property. - * @return string schema and hostname part (with port number if needed) of the request URL (e.g. `http://www.yiiframework.com`) - * @see setHostInfo() - */ - public function getHostInfo() - { - if ($this->_hostInfo === null) { - $secure = $this->getIsSecureConnection(); - $http = $secure ? 'https' : 'http'; - if (isset($_SERVER['HTTP_HOST'])) { - $this->_hostInfo = $http . '://' . $_SERVER['HTTP_HOST']; - } else { - $this->_hostInfo = $http . '://' . $_SERVER['SERVER_NAME']; - $port = $secure ? $this->getSecurePort() : $this->getPort(); - if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) { - $this->_hostInfo .= ':' . $port; - } - } - } - - return $this->_hostInfo; - } - - /** - * Sets the schema and host part of the application URL. - * This setter is provided in case the schema and hostname cannot be determined - * on certain Web servers. - * @param string $value the schema and host part of the application URL. The trailing slashes will be removed. - */ - public function setHostInfo($value) - { - $this->_hostInfo = rtrim($value, '/'); - } - - private $_baseUrl; - - /** - * Returns the relative URL for the application. - * This is similar to [[scriptUrl]] except that it does not include the script file name, - * and the ending slashes are removed. - * @return string the relative URL for the application - * @see setScriptUrl() - */ - public function getBaseUrl() - { - if ($this->_baseUrl === null) { - $this->_baseUrl = rtrim(dirname($this->getScriptUrl()), '\\/'); - } - return $this->_baseUrl; - } - - /** - * Sets the relative URL for the application. - * By default the URL is determined based on the entry script URL. - * This setter is provided in case you want to change this behavior. - * @param string $value the relative URL for the application - */ - public function setBaseUrl($value) - { - $this->_baseUrl = $value; - } - - private $_scriptUrl; - - /** - * Returns the relative URL of the entry script. - * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. - * @return string the relative URL of the entry script. - * @throws InvalidConfigException if unable to determine the entry script URL - */ - public function getScriptUrl() - { - if ($this->_scriptUrl === null) { - $scriptFile = $this->getScriptFile(); - $scriptName = basename($scriptFile); - if (basename($_SERVER['SCRIPT_NAME']) === $scriptName) { - $this->_scriptUrl = $_SERVER['SCRIPT_NAME']; - } elseif (basename($_SERVER['PHP_SELF']) === $scriptName) { - $this->_scriptUrl = $_SERVER['PHP_SELF']; - } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $scriptName) { - $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME']; - } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) { - $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName; - } elseif (!empty($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) { - $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile)); - } else { - throw new InvalidConfigException('Unable to determine the entry script URL.'); - } - } - return $this->_scriptUrl; - } - - /** - * Sets the relative URL for the application entry script. - * This setter is provided in case the entry script URL cannot be determined - * on certain Web servers. - * @param string $value the relative URL for the application entry script. - */ - public function setScriptUrl($value) - { - $this->_scriptUrl = '/' . trim($value, '/'); - } - - private $_scriptFile; - - /** - * Returns the entry script file path. - * The default implementation will simply return `$_SERVER['SCRIPT_FILENAME']`. - * @return string the entry script file path - */ - public function getScriptFile() - { - return isset($this->_scriptFile) ? $this->_scriptFile : $_SERVER['SCRIPT_FILENAME']; - } - - /** - * Sets the entry script file path. - * The entry script file path normally can be obtained from `$_SERVER['SCRIPT_FILENAME']`. - * If your server configuration does not return the correct value, you may configure - * this property to make it right. - * @param string $value the entry script file path. - */ - public function setScriptFile($value) - { - $this->_scriptFile = $value; - } - - private $_pathInfo; - - /** - * Returns the path info of the currently requested URL. - * A path info refers to the part that is after the entry script and before the question mark (query string). - * The starting and ending slashes are both removed. - * @return string part of the request URL that is after the entry script and before the question mark. - * Note, the returned path info is already URL-decoded. - * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration - */ - public function getPathInfo() - { - if ($this->_pathInfo === null) { - $this->_pathInfo = $this->resolvePathInfo(); - } - return $this->_pathInfo; - } - - /** - * Sets the path info of the current request. - * This method is mainly provided for testing purpose. - * @param string $value the path info of the current request - */ - public function setPathInfo($value) - { - $this->_pathInfo = ltrim($value, '/'); - } - - /** - * Resolves the path info part of the currently requested URL. - * A path info refers to the part that is after the entry script and before the question mark (query string). - * The starting slashes are both removed (ending slashes will be kept). - * @return string part of the request URL that is after the entry script and before the question mark. - * Note, the returned path info is decoded. - * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration - */ - protected function resolvePathInfo() - { - $pathInfo = $this->getUrl(); - - if (($pos = strpos($pathInfo, '?')) !== false) { - $pathInfo = substr($pathInfo, 0, $pos); - } - - $pathInfo = urldecode($pathInfo); - - // try to encode in UTF8 if not so - // http://w3.org/International/questions/qa-forms-utf-8.html - if (!preg_match('%^(?: - [\x09\x0A\x0D\x20-\x7E] # ASCII - | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte - | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs - | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte - | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates - | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 - | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 - | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 - )*$%xs', $pathInfo)) { - $pathInfo = utf8_encode($pathInfo); - } - - $scriptUrl = $this->getScriptUrl(); - $baseUrl = $this->getBaseUrl(); - if (strpos($pathInfo, $scriptUrl) === 0) { - $pathInfo = substr($pathInfo, strlen($scriptUrl)); - } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { - $pathInfo = substr($pathInfo, strlen($baseUrl)); - } elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) { - $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); - } else { - throw new InvalidConfigException('Unable to determine the path info of the current request.'); - } - - if ($pathInfo[0] === '/') { - $pathInfo = substr($pathInfo, 1); - } - - return (string)$pathInfo; - } - - /** - * Returns the currently requested absolute URL. - * This is a shortcut to the concatenation of [[hostInfo]] and [[url]]. - * @return string the currently requested absolute URL. - */ - public function getAbsoluteUrl() - { - return $this->getHostInfo() . $this->getUrl(); - } - - private $_url; - - /** - * Returns the currently requested relative URL. - * This refers to the portion of the URL that is after the [[hostInfo]] part. - * It includes the [[queryString]] part if any. - * @return string the currently requested relative URL. Note that the URI returned is URL-encoded. - * @throws InvalidConfigException if the URL cannot be determined due to unusual server configuration - */ - public function getUrl() - { - if ($this->_url === null) { - $this->_url = $this->resolveRequestUri(); - } - return $this->_url; - } - - /** - * Sets the currently requested relative URL. - * The URI must refer to the portion that is after [[hostInfo]]. - * Note that the URI should be URL-encoded. - * @param string $value the request URI to be set - */ - public function setUrl($value) - { - $this->_url = $value; - } - - /** - * Resolves the request URI portion for the currently requested URL. - * This refers to the portion that is after the [[hostInfo]] part. It includes the [[queryString]] part if any. - * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. - * @return string|boolean the request URI portion for the currently requested URL. - * Note that the URI returned is URL-encoded. - * @throws InvalidConfigException if the request URI cannot be determined due to unusual server configuration - */ - protected function resolveRequestUri() - { - if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // IIS - $requestUri = $_SERVER['HTTP_X_REWRITE_URL']; - } elseif (isset($_SERVER['REQUEST_URI'])) { - $requestUri = $_SERVER['REQUEST_URI']; - if ($requestUri !== '' && $requestUri[0] !== '/') { - $requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i', '', $requestUri); - } - } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 CGI - $requestUri = $_SERVER['ORIG_PATH_INFO']; - if (!empty($_SERVER['QUERY_STRING'])) { - $requestUri .= '?' . $_SERVER['QUERY_STRING']; - } - } else { - throw new InvalidConfigException('Unable to determine the request URI.'); - } - return $requestUri; - } - - /** - * Returns part of the request URL that is after the question mark. - * @return string part of the request URL that is after the question mark - */ - public function getQueryString() - { - return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; - } - - /** - * Return if the request is sent via secure channel (https). - * @return boolean if the request is sent via secure channel (https) - */ - public function getIsSecureConnection() - { - return isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1) - || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0; - } - - /** - * Returns the server name. - * @return string server name - */ - public function getServerName() - { - return $_SERVER['SERVER_NAME']; - } - - /** - * Returns the server port number. - * @return integer server port number - */ - public function getServerPort() - { - return (int)$_SERVER['SERVER_PORT']; - } - - /** - * Returns the URL referrer, null if not present - * @return string URL referrer, null if not present - */ - public function getReferrer() - { - return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; - } - - /** - * Returns the user agent, null if not present. - * @return string user agent, null if not present - */ - public function getUserAgent() - { - return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null; - } - - /** - * Returns the user IP address. - * @return string user IP address - */ - public function getUserIP() - { - return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; - } - - /** - * Returns the user host name, null if it cannot be determined. - * @return string user host name, null if cannot be determined - */ - public function getUserHost() - { - return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null; - } - - /** - * @return string the username sent via HTTP authentication, null if the username is not given - */ - public function getAuthUser() - { - return isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; - } - - /** - * @return string the password sent via HTTP authentication, null if the password is not given - */ - public function getAuthPassword() - { - return isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; - } - - private $_port; - - /** - * Returns the port to use for insecure requests. - * Defaults to 80, or the port specified by the server if the current - * request is insecure. - * @return integer port number for insecure requests. - * @see setPort() - */ - public function getPort() - { - if ($this->_port === null) { - $this->_port = !$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 80; - } - return $this->_port; - } - - /** - * Sets the port to use for insecure requests. - * This setter is provided in case a custom port is necessary for certain - * server configurations. - * @param integer $value port number. - */ - public function setPort($value) - { - if ($value != $this->_port) { - $this->_port = (int)$value; - $this->_hostInfo = null; - } - } - - private $_securePort; - - /** - * Returns the port to use for secure requests. - * Defaults to 443, or the port specified by the server if the current - * request is secure. - * @return integer port number for secure requests. - * @see setSecurePort() - */ - public function getSecurePort() - { - if ($this->_securePort === null) { - $this->_securePort = $this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 443; - } - return $this->_securePort; - } - - /** - * Sets the port to use for secure requests. - * This setter is provided in case a custom port is necessary for certain - * server configurations. - * @param integer $value port number. - */ - public function setSecurePort($value) - { - if ($value != $this->_securePort) { - $this->_securePort = (int)$value; - $this->_hostInfo = null; - } - } - - private $_contentTypes; - - /** - * Returns the content types acceptable by the end user. - * This is determined by the `Accept` HTTP header. For example, - * - * ```php - * $_SERVER['HTTP_ACCEPT'] = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;'; - * $types = $request->getAcceptableContentTypes(); - * print_r($types); - * // displays: - * // [ - * // 'application/json' => ['q' => 1, 'version' => '1.0'], - * // 'application/xml' => ['q' => 1, 'version' => '2.0'], - * // 'text/plain' => ['q' => 0.5], - * // ] - * ``` - * - * @return array the content types ordered by the quality score. Types with the highest scores - * will be returned first. The array keys are the content types, while the array values - * are the corresponding quality score and other parameters as given in the header. - */ - public function getAcceptableContentTypes() - { - if ($this->_contentTypes === null) { - if (isset($_SERVER['HTTP_ACCEPT'])) { - $this->_contentTypes = $this->parseAcceptHeader($_SERVER['HTTP_ACCEPT']); - } else { - $this->_contentTypes = []; - } - } - return $this->_contentTypes; - } - - /** - * Sets the acceptable content types. - * Please refer to [[getAcceptableContentTypes()]] on the format of the parameter. - * @param array $value the content types that are acceptable by the end user. They should - * be ordered by the preference level. - * @see getAcceptableContentTypes() - * @see parseAcceptHeader() - */ - public function setAcceptableContentTypes($value) - { - $this->_contentTypes = $value; - } - - /** - * Returns request content-type - * The Content-Type header field indicates the MIME type of the data - * contained in [[getRawBody()]] or, in the case of the HEAD method, the - * media type that would have been sent had the request been a GET. - * For the MIME-types the user expects in response, see [[acceptableContentTypes]]. - * @return string request content-type. Null is returned if this information is not available. - * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 - * HTTP 1.1 header field definitions - */ - public function getContentType() - { - if (isset($_SERVER["CONTENT_TYPE"])) { - return $_SERVER["CONTENT_TYPE"]; - } elseif (isset($_SERVER["HTTP_CONTENT_TYPE"])) { //fix bug https://bugs.php.net/bug.php?id=66606 - return $_SERVER["HTTP_CONTENT_TYPE"]; - } - return null; - } - - private $_languages; - - /** - * Returns the languages acceptable by the end user. - * This is determined by the `Accept-Language` HTTP header. - * @return array the languages ordered by the preference level. The first element - * represents the most preferred language. - */ - public function getAcceptableLanguages() - { - if ($this->_languages === null) { - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $this->_languages = array_keys($this->parseAcceptHeader($_SERVER['HTTP_ACCEPT_LANGUAGE'])); - } else { - $this->_languages = []; - } - } - return $this->_languages; - } - - /** - * @param array $value the languages that are acceptable by the end user. They should - * be ordered by the preference level. - */ - public function setAcceptableLanguages($value) - { - $this->_languages = $value; - } - - /** - * Parses the given `Accept` (or `Accept-Language`) header. - * - * This method will return the acceptable values with their quality scores and the corresponding parameters - * as specified in the given `Accept` header. The array keys of the return value are the acceptable values, - * while the array values consisting of the corresponding quality scores and parameters. The acceptable - * values with the highest quality scores will be returned first. For example, - * - * ```php - * $header = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;'; - * $accepts = $request->parseAcceptHeader($header); - * print_r($accepts); - * // displays: - * // [ - * // 'application/json' => ['q' => 1, 'version' => '1.0'], - * // 'application/xml' => ['q' => 1, 'version' => '2.0'], - * // 'text/plain' => ['q' => 0.5], - * // ] - * ``` - * - * @param string $header the header to be parsed - * @return array the acceptable values ordered by their quality score. The values with the highest scores - * will be returned first. - */ - public function parseAcceptHeader($header) - { - $accepts = []; - foreach (explode(',', $header) as $i => $part) { - $params = preg_split('/\s*;\s*/', trim($part), -1, PREG_SPLIT_NO_EMPTY); - if (empty($params)) { - continue; - } - $values = [ - 'q' => [$i, array_shift($params), 1], - ]; - foreach ($params as $param) { - if (strpos($param, '=') !== false) { - list ($key, $value) = explode('=', $param, 2); - if ($key === 'q') { - $values['q'][2] = (double)$value; - } else { - $values[$key] = $value; - } - } else { - $values[] = $param; - } - } - $accepts[] = $values; - } - - usort($accepts, function ($a, $b) { - $a = $a['q']; // index, name, q - $b = $b['q']; - if ($a[2] > $b[2]) { - return -1; - } elseif ($a[2] < $b[2]) { - return 1; - } elseif ($a[1] === $b[1]) { - return $a[0] > $b[0] ? 1 : -1; - } elseif ($a[1] === '*/*') { - return 1; - } elseif ($b[1] === '*/*') { - return -1; - } else { - $wa = $a[1][strlen($a[1]) - 1] === '*'; - $wb = $b[1][strlen($b[1]) - 1] === '*'; - if ($wa xor $wb) { - return $wa ? 1 : -1; - } else { - return $a[0] > $b[0] ? 1 : -1; - } - } - }); - - $result = []; - foreach ($accepts as $accept) { - $name = $accept['q'][1]; - $accept['q'] = $accept['q'][2]; - $result[$name] = $accept; - } - - return $result; - } - - /** - * Returns the user-preferred language that should be used by this application. - * The language resolution is based on the user preferred languages and the languages - * supported by the application. The method will try to find the best match. - * @param array $languages a list of the languages supported by the application. If this is empty, the current - * application language will be returned without further processing. - * @return string the language that the application should use. - */ - public function getPreferredLanguage(array $languages = []) - { - if (empty($languages)) { - return Yii::$app->language; - } - foreach ($this->getAcceptableLanguages() as $acceptableLanguage) { - $acceptableLanguage = str_replace('_', '-', strtolower($acceptableLanguage)); - foreach ($languages as $language) { - $language = str_replace('_', '-', strtolower($language)); - // en-us==en-us, en==en-us, en-us==en - if ($language === $acceptableLanguage || strpos($acceptableLanguage, $language . '-') === 0 || strpos($language, $acceptableLanguage . '-') === 0) { - return $language; - } - } - } - return reset($languages); - } - - /** - * Returns the cookie collection. - * Through the returned cookie collection, you may access a cookie using the following syntax: - * - * ~~~ - * $cookie = $request->cookies['name'] - * if ($cookie !== null) { - * $value = $cookie->value; - * } - * - * // alternatively - * $value = $request->cookies->getValue('name'); - * ~~~ - * - * @return CookieCollection the cookie collection. - */ - public function getCookies() - { - if ($this->_cookies === null) { - $this->_cookies = new CookieCollection($this->loadCookies(), [ - 'readOnly' => true, - ]); - } - return $this->_cookies; - } - - /** - * Converts `$_COOKIE` into an array of [[Cookie]]. - * @return array the cookies obtained from request - */ - protected function loadCookies() - { - $cookies = []; - if ($this->enableCookieValidation) { - $key = $this->getCookieValidationKey(); - foreach ($_COOKIE as $name => $value) { - if (is_string($value) && ($value = Security::validateData($value, $key)) !== false) { - $cookies[$name] = new Cookie([ - 'name' => $name, - 'value' => @unserialize($value), - ]); - } - } - } else { - foreach ($_COOKIE as $name => $value) { - $cookies[$name] = new Cookie([ - 'name' => $name, - 'value' => $value, - ]); - } - } - return $cookies; - } - - private $_cookieValidationKey; - - /** - * @return string the secret key used for cookie validation. If it was not set previously, - * a random key will be generated and used. - */ - public function getCookieValidationKey() - { - if ($this->_cookieValidationKey === null) { - $this->_cookieValidationKey = Security::getSecretKey(__CLASS__ . '/' . Yii::$app->id); - } - return $this->_cookieValidationKey; - } - - /** - * Sets the secret key used for cookie validation. - * @param string $value the secret key used for cookie validation. - */ - public function setCookieValidationKey($value) - { - $this->_cookieValidationKey = $value; - } - - /** - * @var Cookie - */ - private $_csrfCookie; - - /** - * Returns the unmasked random token used to perform CSRF validation. - * This token is typically sent via a cookie. If such a cookie does not exist, a new token will be generated. - * @return string the random token for CSRF validation. - * @see enableCsrfValidation - */ - public function getRawCsrfToken() - { - if ($this->_csrfCookie === null) { - $this->_csrfCookie = $this->getCookies()->get($this->csrfParam); - if ($this->_csrfCookie === null) { - $this->_csrfCookie = $this->createCsrfCookie(); - Yii::$app->getResponse()->getCookies()->add($this->_csrfCookie); - } - } - - return $this->_csrfCookie->value; - } - - private $_csrfToken; - - /** - * Returns the token used to perform CSRF validation. - * - * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/). - * This token may be passed along via a hidden field of an HTML form or an HTTP header value - * to support CSRF validation. - * - * @return string the token used to perform CSRF validation. - */ - public function getCsrfToken() - { - if ($this->_csrfToken === null) { - // the mask doesn't need to be very random - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; - $mask = substr(str_shuffle(str_repeat($chars, 5)), 0, self::CSRF_MASK_LENGTH); - - $token = $this->getRawCsrfToken(); - // The + sign may be decoded as blank space later, which will fail the validation - $this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask))); - } - return $this->_csrfToken; - } - - /** - * Returns the XOR result of two strings. - * If the two strings are of different lengths, the shorter one will be padded to the length of the longer one. - * @param string $token1 - * @param string $token2 - * @return string the XOR result - */ - private function xorTokens($token1, $token2) - { - $n1 = StringHelper::byteLength($token1); - $n2 = StringHelper::byteLength($token2); - if ($n1 > $n2) { - $token2 = str_pad($token2, $n1, $token2); - } elseif ($n1 < $n2) { - $token1 = str_pad($token1, $n2, $token1); - } - return $token1 ^ $token2; - } - - /** - * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. - */ - public function getCsrfTokenFromHeader() - { - $key = 'HTTP_' . str_replace('-', '_', strtoupper(self::CSRF_HEADER)); - return isset($_SERVER[$key]) ? $_SERVER[$key] : null; - } - - /** - * Creates a cookie with a randomly generated CSRF token. - * Initial values specified in [[csrfCookie]] will be applied to the generated cookie. - * @return Cookie the generated cookie - * @see enableCsrfValidation - */ - protected function createCsrfCookie() - { - $options = $this->csrfCookie; - $options['name'] = $this->csrfParam; - $options['value'] = Security::generateRandomKey(); - return new Cookie($options); - } - - /** - * Performs the CSRF validation. - * The method will compare the CSRF token obtained from a cookie and from a POST field. - * If they are different, a CSRF attack is detected and a 400 HTTP exception will be raised. - * This method is called in [[Controller::beforeAction()]]. - * @return boolean whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true. - */ - public function validateCsrfToken() - { - $method = $this->getMethod(); - // only validate CSRF token on non-"safe" methods http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 - if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { - return true; - } - $trueToken = $this->getCookies()->getValue($this->csrfParam); - $token = $this->getBodyParam($this->csrfParam); - return $this->validateCsrfTokenInternal($token, $trueToken) - || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); - } - - private function validateCsrfTokenInternal($token, $trueToken) - { - $token = base64_decode(str_replace('.', '+', $token)); - $n = StringHelper::byteLength($token); - if ($n <= self::CSRF_MASK_LENGTH) { - return false; - } - $mask = StringHelper::byteSubstr($token, 0, self::CSRF_MASK_LENGTH); - $token = StringHelper::byteSubstr($token, self::CSRF_MASK_LENGTH, $n - self::CSRF_MASK_LENGTH); - $token = $this->xorTokens($mask, $token); - return $token === $trueToken; - } + /** + * The name of the HTTP header for sending CSRF token. + */ + const CSRF_HEADER = 'X-CSRF-Token'; + /** + * The length of the CSRF token mask. + */ + const CSRF_MASK_LENGTH = 8; + + /** + * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to true. + * When CSRF validation is enabled, forms submitted to an Yii Web application must be originated + * from the same application. If not, a 400 HTTP exception will be raised. + * + * Note, this feature requires that the user client accepts cookie. Also, to use this feature, + * forms submitted via POST method must contain a hidden input whose name is specified by [[csrfParam]]. + * You may use [[\yii\web\Html::beginForm()]] to generate his hidden input. + * + * In JavaScript, you may get the values of [[csrfParam]] and [[csrfToken]] via `yii.getCsrfParam()` and + * `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered. + * + * @see Controller::enableCsrfValidation + * @see http://en.wikipedia.org/wiki/Cross-site_request_forgery + */ + public $enableCsrfValidation = true; + /** + * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'. + * This property is used only when [[enableCsrfValidation]] is true. + */ + public $csrfParam = '_csrf'; + /** + * @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. + * @see Cookie + */ + public $csrfCookie = ['httpOnly' => true]; + /** + * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. + */ + public $enableCookieValidation = true; + /** + * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT, PATCH or DELETE + * request tunneled through POST. Default to '_method'. + * @see getMethod() + * @see getBodyParams() + */ + public $methodParam = '_method'; + /** + * @var array the parsers for converting the raw HTTP request body into [[bodyParams]]. + * The array keys are the request `Content-Types`, and the array values are the + * corresponding configurations for [[Yii::createObject|creating the parser objects]]. + * A parser must implement the [[RequestParserInterface]]. + * + * To enable parsing for JSON requests you can use the [[JsonParser]] class like in the following example: + * + * ``` + * [ + * 'application/json' => 'yii\web\JsonParser', + * ] + * ``` + * + * To register a parser for parsing all request types you can use `'*'` as the array key. + * This one will be used as a fallback in case no other types match. + * + * @see getBodyParams() + */ + public $parsers = []; + + /** + * @var CookieCollection Collection of request cookies. + */ + private $_cookies; + /** + * @var array the headers in this collection (indexed by the header names) + */ + private $_headers; + + /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + * @throws HttpException if the request cannot be resolved. + */ + public function resolve() + { + $result = Yii::$app->getUrlManager()->parseRequest($this); + if ($result !== false) { + list ($route, $params) = $result; + $_GET = array_merge($_GET, $params); + + return [$route, $_GET]; + } else { + throw new NotFoundHttpException(Yii::t('yii', 'Page not found.')); + } + } + + /** + * Returns the header collection. + * The header collection contains incoming HTTP headers. + * @return HeaderCollection the header collection + */ + public function getHeaders() + { + if ($this->_headers === null) { + $this->_headers = new HeaderCollection; + if (function_exists('getallheaders')) { + $headers = getallheaders(); + } elseif (function_exists('http_get_request_headers')) { + $headers = http_get_request_headers(); + } else { + foreach ($_SERVER as $name => $value) { + if (strncmp($name, 'HTTP_', 5) === 0) { + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); + $this->_headers->add($name, $value); + } + } + + return $this->_headers; + } + foreach ($headers as $name => $value) { + $this->_headers->add($name, $value); + } + } + + return $this->_headers; + } + + /** + * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, PATCH, DELETE). + * @return string request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. + * The value returned is turned into upper case. + */ + public function getMethod() + { + if (isset($_POST[$this->methodParam])) { + return strtoupper($_POST[$this->methodParam]); + } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } else { + return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + } + } + + /** + * Returns whether this is a GET request. + * @return boolean whether this is a GET request. + */ + public function getIsGet() + { + return $this->getMethod() === 'GET'; + } + + /** + * Returns whether this is an OPTIONS request. + * @return boolean whether this is a OPTIONS request. + */ + public function getIsOptions() + { + return $this->getMethod() === 'OPTIONS'; + } + + /** + * Returns whether this is a HEAD request. + * @return boolean whether this is a HEAD request. + */ + public function getIsHead() + { + return $this->getMethod() === 'HEAD'; + } + + /** + * Returns whether this is a POST request. + * @return boolean whether this is a POST request. + */ + public function getIsPost() + { + return $this->getMethod() === 'POST'; + } + + /** + * Returns whether this is a DELETE request. + * @return boolean whether this is a DELETE request. + */ + public function getIsDelete() + { + return $this->getMethod() === 'DELETE'; + } + + /** + * Returns whether this is a PUT request. + * @return boolean whether this is a PUT request. + */ + public function getIsPut() + { + return $this->getMethod() === 'PUT'; + } + + /** + * Returns whether this is a PATCH request. + * @return boolean whether this is a PATCH request. + */ + public function getIsPatch() + { + return $this->getMethod() === 'PATCH'; + } + + /** + * Returns whether this is an AJAX (XMLHttpRequest) request. + * @return boolean whether this is an AJAX (XMLHttpRequest) request. + */ + public function getIsAjax() + { + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; + } + + /** + * Returns whether this is a PJAX request + * @return boolean whether this is a PJAX request + */ + public function getIsPjax() + { + return $this->getIsAjax() && !empty($_SERVER['HTTP_X_PJAX']); + } + + /** + * Returns whether this is an Adobe Flash or Flex request. + * @return boolean whether this is an Adobe Flash or Adobe Flex request. + */ + public function getIsFlash() + { + return isset($_SERVER['HTTP_USER_AGENT']) && + (stripos($_SERVER['HTTP_USER_AGENT'], 'Shockwave') !== false || stripos($_SERVER['HTTP_USER_AGENT'], 'Flash') !== false); + } + + private $_rawBody; + + /** + * Returns the raw HTTP request body. + * @return string the request body + */ + public function getRawBody() + { + if ($this->_rawBody === null) { + $this->_rawBody = file_get_contents('php://input'); + } + + return $this->_rawBody; + } + + private $_bodyParams; + + /** + * Returns the request parameters given in the request body. + * + * Request parameters are determined using the parsers configured in [[parsers]] property. + * If no parsers are configured for the current [[contentType]] it uses the PHP function [[mb_parse_str()]] + * to parse the [[rawBody|request body]]. + * @return array the request parameters given in the request body. + * @throws \yii\base\InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]]. + * @see getMethod() + * @see getBodyParam() + * @see setBodyParams() + */ + public function getBodyParams() + { + if ($this->_bodyParams === null) { + $contentType = $this->getContentType(); + if (isset($_POST[$this->methodParam])) { + $this->_bodyParams = $_POST; + unset($this->_bodyParams[$this->methodParam]); + } elseif (isset($this->parsers[$contentType])) { + $parser = Yii::createObject($this->parsers[$contentType]); + if (!($parser instanceof RequestParserInterface)) { + throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface."); + } + $this->_bodyParams = $parser->parse($this->getRawBody(), $contentType); + } elseif (isset($this->parsers['*'])) { + $parser = Yii::createObject($this->parsers['*']); + if (!($parser instanceof RequestParserInterface)) { + throw new InvalidConfigException("The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface."); + } + $this->_bodyParams = $parser->parse($this->getRawBody(), $contentType); + } elseif ($this->getMethod() === 'POST') { + // PHP has already parsed the body so we have all params in $_POST + $this->_bodyParams = $_POST; + } else { + $this->_bodyParams = []; + mb_parse_str($this->getRawBody(), $this->_bodyParams); + } + } + + return $this->_bodyParams; + } + + /** + * Sets the request body parameters. + * @param array $values the request body parameters (name-value pairs) + * @see getBodyParam() + * @see getBodyParams() + */ + public function setBodyParams($values) + { + $this->_bodyParams = $values; + } + + /** + * Returns the named request body parameter value. + * @param string $name the parameter name + * @param mixed $defaultValue the default parameter value if the parameter does not exist. + * @return mixed the parameter value + * @see getBodyParams() + * @see setBodyParams() + */ + public function getBodyParam($name, $defaultValue = null) + { + $params = $this->getBodyParams(); + + return isset($params[$name]) ? $params[$name] : $defaultValue; + } + + /** + * Returns POST parameter with a given name. If name isn't specified, returns an array of all POST parameters. + * + * @param string $name the parameter name + * @param mixed $defaultValue the default parameter value if the parameter does not exist. + * @return array|mixed + */ + public function post($name = null, $defaultValue = null) + { + if ($name === null) { + return $this->getBodyParams(); + } else { + return $this->getBodyParam($name, $defaultValue); + } + } + + private $_queryParams; + + /** + * Returns the request parameters given in the [[queryString]]. + * + * This method will return the contents of `$_GET` if params where not explicitly set. + * @return array the request GET parameter values. + * @see setQueryParams() + */ + public function getQueryParams() + { + if ($this->_queryParams === null) { + return $_GET; + } + + return $this->_queryParams; + } + + /** + * Sets the request [[queryString]] parameters. + * @param array $values the request query parameters (name-value pairs) + * @see getQueryParam() + * @see getQueryParams() + */ + public function setQueryParams($values) + { + $this->_queryParams = $values; + } + + /** + * Returns GET parameter with a given name. If name isn't specified, returns an array of all GET parameters. + * + * @param string $name the parameter name + * @param mixed $defaultValue the default parameter value if the parameter does not exist. + * @return array|mixed + */ + public function get($name = null, $defaultValue = null) + { + if ($name === null) { + return $this->getQueryParams(); + } else { + return $this->getQueryParam($name, $defaultValue); + } + } + + /** + * Returns the named GET parameter value. + * If the GET parameter does not exist, the second parameter to this method will be returned. + * @param string $name the GET parameter name. If not specified, whole $_GET is returned. + * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. + * @return mixed the GET parameter value + * @see getBodyParam() + */ + public function getQueryParam($name, $defaultValue = null) + { + $params = $this->getQueryParams(); + + return isset($params[$name]) ? $params[$name] : $defaultValue; + } + + private $_hostInfo; + + /** + * Returns the schema and host part of the current request URL. + * The returned URL does not have an ending slash. + * By default this is determined based on the user request information. + * You may explicitly specify it by setting the [[setHostInfo()|hostInfo]] property. + * @return string schema and hostname part (with port number if needed) of the request URL (e.g. `http://www.yiiframework.com`) + * @see setHostInfo() + */ + public function getHostInfo() + { + if ($this->_hostInfo === null) { + $secure = $this->getIsSecureConnection(); + $http = $secure ? 'https' : 'http'; + if (isset($_SERVER['HTTP_HOST'])) { + $this->_hostInfo = $http . '://' . $_SERVER['HTTP_HOST']; + } else { + $this->_hostInfo = $http . '://' . $_SERVER['SERVER_NAME']; + $port = $secure ? $this->getSecurePort() : $this->getPort(); + if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) { + $this->_hostInfo .= ':' . $port; + } + } + } + + return $this->_hostInfo; + } + + /** + * Sets the schema and host part of the application URL. + * This setter is provided in case the schema and hostname cannot be determined + * on certain Web servers. + * @param string $value the schema and host part of the application URL. The trailing slashes will be removed. + */ + public function setHostInfo($value) + { + $this->_hostInfo = rtrim($value, '/'); + } + + private $_baseUrl; + + /** + * Returns the relative URL for the application. + * This is similar to [[scriptUrl]] except that it does not include the script file name, + * and the ending slashes are removed. + * @return string the relative URL for the application + * @see setScriptUrl() + */ + public function getBaseUrl() + { + if ($this->_baseUrl === null) { + $this->_baseUrl = rtrim(dirname($this->getScriptUrl()), '\\/'); + } + + return $this->_baseUrl; + } + + /** + * Sets the relative URL for the application. + * By default the URL is determined based on the entry script URL. + * This setter is provided in case you want to change this behavior. + * @param string $value the relative URL for the application + */ + public function setBaseUrl($value) + { + $this->_baseUrl = $value; + } + + private $_scriptUrl; + + /** + * Returns the relative URL of the entry script. + * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. + * @return string the relative URL of the entry script. + * @throws InvalidConfigException if unable to determine the entry script URL + */ + public function getScriptUrl() + { + if ($this->_scriptUrl === null) { + $scriptFile = $this->getScriptFile(); + $scriptName = basename($scriptFile); + if (basename($_SERVER['SCRIPT_NAME']) === $scriptName) { + $this->_scriptUrl = $_SERVER['SCRIPT_NAME']; + } elseif (basename($_SERVER['PHP_SELF']) === $scriptName) { + $this->_scriptUrl = $_SERVER['PHP_SELF']; + } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $scriptName) { + $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME']; + } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) { + $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName; + } elseif (!empty($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) { + $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile)); + } else { + throw new InvalidConfigException('Unable to determine the entry script URL.'); + } + } + + return $this->_scriptUrl; + } + + /** + * Sets the relative URL for the application entry script. + * This setter is provided in case the entry script URL cannot be determined + * on certain Web servers. + * @param string $value the relative URL for the application entry script. + */ + public function setScriptUrl($value) + { + $this->_scriptUrl = '/' . trim($value, '/'); + } + + private $_scriptFile; + + /** + * Returns the entry script file path. + * The default implementation will simply return `$_SERVER['SCRIPT_FILENAME']`. + * @return string the entry script file path + */ + public function getScriptFile() + { + return isset($this->_scriptFile) ? $this->_scriptFile : $_SERVER['SCRIPT_FILENAME']; + } + + /** + * Sets the entry script file path. + * The entry script file path normally can be obtained from `$_SERVER['SCRIPT_FILENAME']`. + * If your server configuration does not return the correct value, you may configure + * this property to make it right. + * @param string $value the entry script file path. + */ + public function setScriptFile($value) + { + $this->_scriptFile = $value; + } + + private $_pathInfo; + + /** + * Returns the path info of the currently requested URL. + * A path info refers to the part that is after the entry script and before the question mark (query string). + * The starting and ending slashes are both removed. + * @return string part of the request URL that is after the entry script and before the question mark. + * Note, the returned path info is already URL-decoded. + * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration + */ + public function getPathInfo() + { + if ($this->_pathInfo === null) { + $this->_pathInfo = $this->resolvePathInfo(); + } + + return $this->_pathInfo; + } + + /** + * Sets the path info of the current request. + * This method is mainly provided for testing purpose. + * @param string $value the path info of the current request + */ + public function setPathInfo($value) + { + $this->_pathInfo = ltrim($value, '/'); + } + + /** + * Resolves the path info part of the currently requested URL. + * A path info refers to the part that is after the entry script and before the question mark (query string). + * The starting slashes are both removed (ending slashes will be kept). + * @return string part of the request URL that is after the entry script and before the question mark. + * Note, the returned path info is decoded. + * @throws InvalidConfigException if the path info cannot be determined due to unexpected server configuration + */ + protected function resolvePathInfo() + { + $pathInfo = $this->getUrl(); + + if (($pos = strpos($pathInfo, '?')) !== false) { + $pathInfo = substr($pathInfo, 0, $pos); + } + + $pathInfo = urldecode($pathInfo); + + // try to encode in UTF8 if not so + // http://w3.org/International/questions/qa-forms-utf-8.html + if (!preg_match('%^(?: + [\x09\x0A\x0D\x20-\x7E] # ASCII + | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte + | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte + | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates + | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 + )*$%xs', $pathInfo)) { + $pathInfo = utf8_encode($pathInfo); + } + + $scriptUrl = $this->getScriptUrl(); + $baseUrl = $this->getBaseUrl(); + if (strpos($pathInfo, $scriptUrl) === 0) { + $pathInfo = substr($pathInfo, strlen($scriptUrl)); + } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { + $pathInfo = substr($pathInfo, strlen($baseUrl)); + } elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) { + $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); + } else { + throw new InvalidConfigException('Unable to determine the path info of the current request.'); + } + + if ($pathInfo[0] === '/') { + $pathInfo = substr($pathInfo, 1); + } + + return (string) $pathInfo; + } + + /** + * Returns the currently requested absolute URL. + * This is a shortcut to the concatenation of [[hostInfo]] and [[url]]. + * @return string the currently requested absolute URL. + */ + public function getAbsoluteUrl() + { + return $this->getHostInfo() . $this->getUrl(); + } + + private $_url; + + /** + * Returns the currently requested relative URL. + * This refers to the portion of the URL that is after the [[hostInfo]] part. + * It includes the [[queryString]] part if any. + * @return string the currently requested relative URL. Note that the URI returned is URL-encoded. + * @throws InvalidConfigException if the URL cannot be determined due to unusual server configuration + */ + public function getUrl() + { + if ($this->_url === null) { + $this->_url = $this->resolveRequestUri(); + } + + return $this->_url; + } + + /** + * Sets the currently requested relative URL. + * The URI must refer to the portion that is after [[hostInfo]]. + * Note that the URI should be URL-encoded. + * @param string $value the request URI to be set + */ + public function setUrl($value) + { + $this->_url = $value; + } + + /** + * Resolves the request URI portion for the currently requested URL. + * This refers to the portion that is after the [[hostInfo]] part. It includes the [[queryString]] part if any. + * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. + * @return string|boolean the request URI portion for the currently requested URL. + * Note that the URI returned is URL-encoded. + * @throws InvalidConfigException if the request URI cannot be determined due to unusual server configuration + */ + protected function resolveRequestUri() + { + if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // IIS + $requestUri = $_SERVER['HTTP_X_REWRITE_URL']; + } elseif (isset($_SERVER['REQUEST_URI'])) { + $requestUri = $_SERVER['REQUEST_URI']; + if ($requestUri !== '' && $requestUri[0] !== '/') { + $requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i', '', $requestUri); + } + } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 CGI + $requestUri = $_SERVER['ORIG_PATH_INFO']; + if (!empty($_SERVER['QUERY_STRING'])) { + $requestUri .= '?' . $_SERVER['QUERY_STRING']; + } + } else { + throw new InvalidConfigException('Unable to determine the request URI.'); + } + + return $requestUri; + } + + /** + * Returns part of the request URL that is after the question mark. + * @return string part of the request URL that is after the question mark + */ + public function getQueryString() + { + return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; + } + + /** + * Return if the request is sent via secure channel (https). + * @return boolean if the request is sent via secure channel (https) + */ + public function getIsSecureConnection() + { + return isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1) + || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0; + } + + /** + * Returns the server name. + * @return string server name + */ + public function getServerName() + { + return $_SERVER['SERVER_NAME']; + } + + /** + * Returns the server port number. + * @return integer server port number + */ + public function getServerPort() + { + return (int) $_SERVER['SERVER_PORT']; + } + + /** + * Returns the URL referrer, null if not present + * @return string URL referrer, null if not present + */ + public function getReferrer() + { + return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; + } + + /** + * Returns the user agent, null if not present. + * @return string user agent, null if not present + */ + public function getUserAgent() + { + return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null; + } + + /** + * Returns the user IP address. + * @return string user IP address + */ + public function getUserIP() + { + return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; + } + + /** + * Returns the user host name, null if it cannot be determined. + * @return string user host name, null if cannot be determined + */ + public function getUserHost() + { + return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null; + } + + /** + * @return string the username sent via HTTP authentication, null if the username is not given + */ + public function getAuthUser() + { + return isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; + } + + /** + * @return string the password sent via HTTP authentication, null if the password is not given + */ + public function getAuthPassword() + { + return isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; + } + + private $_port; + + /** + * Returns the port to use for insecure requests. + * Defaults to 80, or the port specified by the server if the current + * request is insecure. + * @return integer port number for insecure requests. + * @see setPort() + */ + public function getPort() + { + if ($this->_port === null) { + $this->_port = !$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 80; + } + + return $this->_port; + } + + /** + * Sets the port to use for insecure requests. + * This setter is provided in case a custom port is necessary for certain + * server configurations. + * @param integer $value port number. + */ + public function setPort($value) + { + if ($value != $this->_port) { + $this->_port = (int) $value; + $this->_hostInfo = null; + } + } + + private $_securePort; + + /** + * Returns the port to use for secure requests. + * Defaults to 443, or the port specified by the server if the current + * request is secure. + * @return integer port number for secure requests. + * @see setSecurePort() + */ + public function getSecurePort() + { + if ($this->_securePort === null) { + $this->_securePort = $this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 443; + } + + return $this->_securePort; + } + + /** + * Sets the port to use for secure requests. + * This setter is provided in case a custom port is necessary for certain + * server configurations. + * @param integer $value port number. + */ + public function setSecurePort($value) + { + if ($value != $this->_securePort) { + $this->_securePort = (int) $value; + $this->_hostInfo = null; + } + } + + private $_contentTypes; + + /** + * Returns the content types acceptable by the end user. + * This is determined by the `Accept` HTTP header. For example, + * + * ```php + * $_SERVER['HTTP_ACCEPT'] = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;'; + * $types = $request->getAcceptableContentTypes(); + * print_r($types); + * // displays: + * // [ + * // 'application/json' => ['q' => 1, 'version' => '1.0'], + * // 'application/xml' => ['q' => 1, 'version' => '2.0'], + * // 'text/plain' => ['q' => 0.5], + * // ] + * ``` + * + * @return array the content types ordered by the quality score. Types with the highest scores + * will be returned first. The array keys are the content types, while the array values + * are the corresponding quality score and other parameters as given in the header. + */ + public function getAcceptableContentTypes() + { + if ($this->_contentTypes === null) { + if (isset($_SERVER['HTTP_ACCEPT'])) { + $this->_contentTypes = $this->parseAcceptHeader($_SERVER['HTTP_ACCEPT']); + } else { + $this->_contentTypes = []; + } + } + + return $this->_contentTypes; + } + + /** + * Sets the acceptable content types. + * Please refer to [[getAcceptableContentTypes()]] on the format of the parameter. + * @param array $value the content types that are acceptable by the end user. They should + * be ordered by the preference level. + * @see getAcceptableContentTypes() + * @see parseAcceptHeader() + */ + public function setAcceptableContentTypes($value) + { + $this->_contentTypes = $value; + } + + /** + * Returns request content-type + * The Content-Type header field indicates the MIME type of the data + * contained in [[getRawBody()]] or, in the case of the HEAD method, the + * media type that would have been sent had the request been a GET. + * For the MIME-types the user expects in response, see [[acceptableContentTypes]]. + * @return string request content-type. Null is returned if this information is not available. + * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 + * HTTP 1.1 header field definitions + */ + public function getContentType() + { + if (isset($_SERVER["CONTENT_TYPE"])) { + return $_SERVER["CONTENT_TYPE"]; + } elseif (isset($_SERVER["HTTP_CONTENT_TYPE"])) { //fix bug https://bugs.php.net/bug.php?id=66606 + + return $_SERVER["HTTP_CONTENT_TYPE"]; + } + + return null; + } + + private $_languages; + + /** + * Returns the languages acceptable by the end user. + * This is determined by the `Accept-Language` HTTP header. + * @return array the languages ordered by the preference level. The first element + * represents the most preferred language. + */ + public function getAcceptableLanguages() + { + if ($this->_languages === null) { + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $this->_languages = array_keys($this->parseAcceptHeader($_SERVER['HTTP_ACCEPT_LANGUAGE'])); + } else { + $this->_languages = []; + } + } + + return $this->_languages; + } + + /** + * @param array $value the languages that are acceptable by the end user. They should + * be ordered by the preference level. + */ + public function setAcceptableLanguages($value) + { + $this->_languages = $value; + } + + /** + * Parses the given `Accept` (or `Accept-Language`) header. + * + * This method will return the acceptable values with their quality scores and the corresponding parameters + * as specified in the given `Accept` header. The array keys of the return value are the acceptable values, + * while the array values consisting of the corresponding quality scores and parameters. The acceptable + * values with the highest quality scores will be returned first. For example, + * + * ```php + * $header = 'text/plain; q=0.5, application/json; version=1.0, application/xml; version=2.0;'; + * $accepts = $request->parseAcceptHeader($header); + * print_r($accepts); + * // displays: + * // [ + * // 'application/json' => ['q' => 1, 'version' => '1.0'], + * // 'application/xml' => ['q' => 1, 'version' => '2.0'], + * // 'text/plain' => ['q' => 0.5], + * // ] + * ``` + * + * @param string $header the header to be parsed + * @return array the acceptable values ordered by their quality score. The values with the highest scores + * will be returned first. + */ + public function parseAcceptHeader($header) + { + $accepts = []; + foreach (explode(',', $header) as $i => $part) { + $params = preg_split('/\s*;\s*/', trim($part), -1, PREG_SPLIT_NO_EMPTY); + if (empty($params)) { + continue; + } + $values = [ + 'q' => [$i, array_shift($params), 1], + ]; + foreach ($params as $param) { + if (strpos($param, '=') !== false) { + list ($key, $value) = explode('=', $param, 2); + if ($key === 'q') { + $values['q'][2] = (double) $value; + } else { + $values[$key] = $value; + } + } else { + $values[] = $param; + } + } + $accepts[] = $values; + } + + usort($accepts, function ($a, $b) { + $a = $a['q']; // index, name, q + $b = $b['q']; + if ($a[2] > $b[2]) { + return -1; + } elseif ($a[2] < $b[2]) { + return 1; + } elseif ($a[1] === $b[1]) { + return $a[0] > $b[0] ? 1 : -1; + } elseif ($a[1] === '*/*') { + return 1; + } elseif ($b[1] === '*/*') { + return -1; + } else { + $wa = $a[1][strlen($a[1]) - 1] === '*'; + $wb = $b[1][strlen($b[1]) - 1] === '*'; + if ($wa xor $wb) { + return $wa ? 1 : -1; + } else { + return $a[0] > $b[0] ? 1 : -1; + } + } + }); + + $result = []; + foreach ($accepts as $accept) { + $name = $accept['q'][1]; + $accept['q'] = $accept['q'][2]; + $result[$name] = $accept; + } + + return $result; + } + + /** + * Returns the user-preferred language that should be used by this application. + * The language resolution is based on the user preferred languages and the languages + * supported by the application. The method will try to find the best match. + * @param array $languages a list of the languages supported by the application. If this is empty, the current + * application language will be returned without further processing. + * @return string the language that the application should use. + */ + public function getPreferredLanguage(array $languages = []) + { + if (empty($languages)) { + return Yii::$app->language; + } + foreach ($this->getAcceptableLanguages() as $acceptableLanguage) { + $acceptableLanguage = str_replace('_', '-', strtolower($acceptableLanguage)); + foreach ($languages as $language) { + $language = str_replace('_', '-', strtolower($language)); + // en-us==en-us, en==en-us, en-us==en + if ($language === $acceptableLanguage || strpos($acceptableLanguage, $language . '-') === 0 || strpos($language, $acceptableLanguage . '-') === 0) { + return $language; + } + } + } + + return reset($languages); + } + + /** + * Returns the cookie collection. + * Through the returned cookie collection, you may access a cookie using the following syntax: + * + * ~~~ + * $cookie = $request->cookies['name'] + * if ($cookie !== null) { + * $value = $cookie->value; + * } + * + * // alternatively + * $value = $request->cookies->getValue('name'); + * ~~~ + * + * @return CookieCollection the cookie collection. + */ + public function getCookies() + { + if ($this->_cookies === null) { + $this->_cookies = new CookieCollection($this->loadCookies(), [ + 'readOnly' => true, + ]); + } + + return $this->_cookies; + } + + /** + * Converts `$_COOKIE` into an array of [[Cookie]]. + * @return array the cookies obtained from request + */ + protected function loadCookies() + { + $cookies = []; + if ($this->enableCookieValidation) { + $key = $this->getCookieValidationKey(); + foreach ($_COOKIE as $name => $value) { + if (is_string($value) && ($value = Security::validateData($value, $key)) !== false) { + $cookies[$name] = new Cookie([ + 'name' => $name, + 'value' => @unserialize($value), + ]); + } + } + } else { + foreach ($_COOKIE as $name => $value) { + $cookies[$name] = new Cookie([ + 'name' => $name, + 'value' => $value, + ]); + } + } + + return $cookies; + } + + private $_cookieValidationKey; + + /** + * @return string the secret key used for cookie validation. If it was not set previously, + * a random key will be generated and used. + */ + public function getCookieValidationKey() + { + if ($this->_cookieValidationKey === null) { + $this->_cookieValidationKey = Security::getSecretKey(__CLASS__ . '/' . Yii::$app->id); + } + + return $this->_cookieValidationKey; + } + + /** + * Sets the secret key used for cookie validation. + * @param string $value the secret key used for cookie validation. + */ + public function setCookieValidationKey($value) + { + $this->_cookieValidationKey = $value; + } + + /** + * @var Cookie + */ + private $_csrfCookie; + + /** + * Returns the unmasked random token used to perform CSRF validation. + * This token is typically sent via a cookie. If such a cookie does not exist, a new token will be generated. + * @return string the random token for CSRF validation. + * @see enableCsrfValidation + */ + public function getRawCsrfToken() + { + if ($this->_csrfCookie === null) { + $this->_csrfCookie = $this->getCookies()->get($this->csrfParam); + if ($this->_csrfCookie === null) { + $this->_csrfCookie = $this->createCsrfCookie(); + Yii::$app->getResponse()->getCookies()->add($this->_csrfCookie); + } + } + + return $this->_csrfCookie->value; + } + + private $_csrfToken; + + /** + * Returns the token used to perform CSRF validation. + * + * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/). + * This token may be passed along via a hidden field of an HTML form or an HTTP header value + * to support CSRF validation. + * + * @return string the token used to perform CSRF validation. + */ + public function getCsrfToken() + { + if ($this->_csrfToken === null) { + // the mask doesn't need to be very random + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; + $mask = substr(str_shuffle(str_repeat($chars, 5)), 0, self::CSRF_MASK_LENGTH); + + $token = $this->getRawCsrfToken(); + // The + sign may be decoded as blank space later, which will fail the validation + $this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask))); + } + + return $this->_csrfToken; + } + + /** + * Returns the XOR result of two strings. + * If the two strings are of different lengths, the shorter one will be padded to the length of the longer one. + * @param string $token1 + * @param string $token2 + * @return string the XOR result + */ + private function xorTokens($token1, $token2) + { + $n1 = StringHelper::byteLength($token1); + $n2 = StringHelper::byteLength($token2); + if ($n1 > $n2) { + $token2 = str_pad($token2, $n1, $token2); + } elseif ($n1 < $n2) { + $token1 = str_pad($token1, $n2, $token1); + } + + return $token1 ^ $token2; + } + + /** + * @return string the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. + */ + public function getCsrfTokenFromHeader() + { + $key = 'HTTP_' . str_replace('-', '_', strtoupper(self::CSRF_HEADER)); + + return isset($_SERVER[$key]) ? $_SERVER[$key] : null; + } + + /** + * Creates a cookie with a randomly generated CSRF token. + * Initial values specified in [[csrfCookie]] will be applied to the generated cookie. + * @return Cookie the generated cookie + * @see enableCsrfValidation + */ + protected function createCsrfCookie() + { + $options = $this->csrfCookie; + $options['name'] = $this->csrfParam; + $options['value'] = Security::generateRandomKey(); + + return new Cookie($options); + } + + /** + * Performs the CSRF validation. + * The method will compare the CSRF token obtained from a cookie and from a POST field. + * If they are different, a CSRF attack is detected and a 400 HTTP exception will be raised. + * This method is called in [[Controller::beforeAction()]]. + * @return boolean whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true. + */ + public function validateCsrfToken() + { + $method = $this->getMethod(); + // only validate CSRF token on non-"safe" methods http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 + if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { + return true; + } + $trueToken = $this->getCookies()->getValue($this->csrfParam); + $token = $this->getBodyParam($this->csrfParam); + + return $this->validateCsrfTokenInternal($token, $trueToken) + || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); + } + + private function validateCsrfTokenInternal($token, $trueToken) + { + $token = base64_decode(str_replace('.', '+', $token)); + $n = StringHelper::byteLength($token); + if ($n <= self::CSRF_MASK_LENGTH) { + return false; + } + $mask = StringHelper::byteSubstr($token, 0, self::CSRF_MASK_LENGTH); + $token = StringHelper::byteSubstr($token, self::CSRF_MASK_LENGTH, $n - self::CSRF_MASK_LENGTH); + $token = $this->xorTokens($mask, $token); + + return $token === $trueToken; + } } diff --git a/framework/web/RequestParserInterface.php b/framework/web/RequestParserInterface.php index b819b0b964e..371cfba854e 100644 --- a/framework/web/RequestParserInterface.php +++ b/framework/web/RequestParserInterface.php @@ -15,11 +15,11 @@ */ interface RequestParserInterface { - /** - * Parses a HTTP request body. - * @param string $rawBody the raw HTTP request body. - * @param string $contentType the content type specified for the request body. - * @return array parameters parsed from the request body - */ - public function parse($rawBody, $contentType); + /** + * Parses a HTTP request body. + * @param string $rawBody the raw HTTP request body. + * @param string $contentType the content type specified for the request body. + * @return array parameters parsed from the request body + */ + public function parse($rawBody, $contentType); } diff --git a/framework/web/Response.php b/framework/web/Response.php index 611cb8eb71c..b540139ee02 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -60,824 +60,827 @@ */ class Response extends \yii\base\Response { - /** - * @event ResponseEvent an event that is triggered at the beginning of [[send()]]. - */ - const EVENT_BEFORE_SEND = 'beforeSend'; - /** - * @event ResponseEvent an event that is triggered at the end of [[send()]]. - */ - const EVENT_AFTER_SEND = 'afterSend'; - /** - * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]]. - * You may respond to this event to filter the response content before it is sent to the client. - */ - const EVENT_AFTER_PREPARE = 'afterPrepare'; - - const FORMAT_RAW = 'raw'; - const FORMAT_HTML = 'html'; - const FORMAT_JSON = 'json'; - const FORMAT_JSONP = 'jsonp'; - const FORMAT_XML = 'xml'; - - /** - * @var string the response format. This determines how to convert [[data]] into [[content]] - * when the latter is not set. By default, the following formats are supported: - * - * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion. - * No extra HTTP header will be added. - * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion. - * The "Content-Type" header will set as "text/html" if it is not set previously. - * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type" - * header will be set as "application/json". - * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type" - * header will be set as "text/javascript". Note that in this case `$data` must be an array - * with "data" and "callback" elements. The former refers to the actual data to be sent, - * while the latter refers to the name of the JavaScript callback. - * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]] - * for more details. - * - * You may customize the formatting process or support additional formats by configuring [[formatters]]. - * @see formatters - */ - public $format = self::FORMAT_HTML; - /** - * @var array the formatters for converting data into the response content of the specified [[format]]. - * The array keys are the format names, and the array values are the corresponding configurations - * for creating the formatter objects. - * @see format - */ - public $formatters; - /** - * @var mixed the original response data. When this is not null, it will be converted into [[content]] - * according to [[format]] when the response is being sent out. - * @see content - */ - public $data; - /** - * @var string the response content. When [[data]] is not null, it will be converted into [[content]] - * according to [[format]] when the response is being sent out. - * @see data - */ - public $content; - /** - * @var resource|array the stream to be sent. This can be a stream handle or an array of stream handle, - * the begin position and the end position. Note that when this property is set, the [[data]] and [[content]] - * properties will be ignored by [[send()]]. - */ - public $stream; - /** - * @var string the charset of the text response. If not set, it will use - * the value of [[Application::charset]]. - */ - public $charset; - /** - * @var string the HTTP status description that comes together with the status code. - * @see httpStatuses - */ - public $statusText = 'OK'; - /** - * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`, - * or '1.1' if that is not available. - */ - public $version; - /** - * @var boolean whether the response has been sent. If this is true, calling [[send()]] will do nothing. - */ - public $isSent = false; - /** - * @var array list of HTTP status codes and the corresponding texts - */ - public static $httpStatuses = [ - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - 118 => 'Connection timed out', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', - 208 => 'Already Reported', - 210 => 'Content Different', - 226 => 'IM Used', - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 306 => 'Reserved', - 307 => 'Temporary Redirect', - 308 => 'Permanent Redirect', - 310 => 'Too many Redirect', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested range unsatisfiable', - 417 => 'Expectation failed', - 418 => 'I\'m a teapot', - 422 => 'Unprocessable entity', - 423 => 'Locked', - 424 => 'Method failure', - 425 => 'Unordered Collection', - 426 => 'Upgrade Required', - 428 => 'Precondition Required', - 429 => 'Too Many Requests', - 431 => 'Request Header Fields Too Large', - 449 => 'Retry With', - 450 => 'Blocked by Windows Parental Controls', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway ou Proxy Error', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported', - 507 => 'Insufficient storage', - 508 => 'Loop Detected', - 509 => 'Bandwidth Limit Exceeded', - 510 => 'Not Extended', - 511 => 'Network Authentication Required', - ]; - - /** - * @var integer the HTTP status code to send with the response. - */ - private $_statusCode = 200; - /** - * @var HeaderCollection - */ - private $_headers; - - /** - * Initializes this component. - */ - public function init() - { - if ($this->version === null) { - if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === '1.0') { - $this->version = '1.0'; - } else { - $this->version = '1.1'; - } - } - if ($this->charset === null) { - $this->charset = Yii::$app->charset; - } - } - - /** - * @return integer the HTTP status code to send with the response. - */ - public function getStatusCode() - { - return $this->_statusCode; - } - - /** - * Sets the response status code. - * This method will set the corresponding status text if `$text` is null. - * @param integer $value the status code - * @param string $text the status text. If not set, it will be set automatically based on the status code. - * @throws InvalidParamException if the status code is invalid. - */ - public function setStatusCode($value, $text = null) - { - if ($value === null) { - $value = 200; - } - $this->_statusCode = (int)$value; - if ($this->getIsInvalid()) { - throw new InvalidParamException("The HTTP status code is invalid: $value"); - } - if ($text === null) { - $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : ''; - } else { - $this->statusText = $text; - } - } - - /** - * Returns the header collection. - * The header collection contains the currently registered HTTP headers. - * @return HeaderCollection the header collection - */ - public function getHeaders() - { - if ($this->_headers === null) { - $this->_headers = new HeaderCollection; - } - return $this->_headers; - } - - /** - * Sends the response to the client. - */ - public function send() - { - if ($this->isSent) { - return; - } - $this->trigger(self::EVENT_BEFORE_SEND); - $this->prepare(); - $this->trigger(self::EVENT_AFTER_PREPARE); - $this->sendHeaders(); - $this->sendContent(); - $this->trigger(self::EVENT_AFTER_SEND); - $this->isSent = true; - } - - /** - * Clears the headers, cookies, content, status code of the response. - */ - public function clear() - { - $this->_headers = null; - $this->_cookies = null; - $this->_statusCode = 200; - $this->statusText = 'OK'; - $this->data = null; - $this->stream = null; - $this->content = null; - $this->isSent = false; - } - - /** - * Sends the response headers to the client - */ - protected function sendHeaders() - { - if (headers_sent()) { - return; - } - $statusCode = $this->getStatusCode(); - header("HTTP/{$this->version} $statusCode {$this->statusText}"); - if ($this->_headers) { - $headers = $this->getHeaders(); - foreach ($headers as $name => $values) { - $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); - foreach ($values as $value) { - header("$name: $value", false); - } - } - } - $this->sendCookies(); - } - - /** - * Sends the cookies to the client. - */ - protected function sendCookies() - { - if ($this->_cookies === null) { - return; - } - $request = Yii::$app->getRequest(); - if ($request->enableCookieValidation) { - $validationKey = $request->getCookieValidationKey(); - } - foreach ($this->getCookies() as $cookie) { - $value = $cookie->value; - if ($cookie->expire != 1 && isset($validationKey)) { - $value = Security::hashData(serialize($value), $validationKey); - } - setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); - } - $this->getCookies()->removeAll(); - } - - /** - * Sends the response content to the client - */ - protected function sendContent() - { - if ($this->stream === null) { - echo $this->content; - return; - } - - set_time_limit(0); // Reset time limit for big files - $chunkSize = 8 * 1024 * 1024; // 8MB per chunk - - if (is_array($this->stream)) { - list ($handle, $begin, $end) = $this->stream; - fseek($handle, $begin); - while (!feof($handle) && ($pos = ftell($handle)) <= $end) { - if ($pos + $chunkSize > $end) { - $chunkSize = $end - $pos + 1; - } - echo fread($handle, $chunkSize); - flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. - } - fclose($handle); - } else { - while (!feof($this->stream)) { - echo fread($this->stream, $chunkSize); - flush(); - } - fclose($this->stream); - } - } - - /** - * Sends a file to the browser. - * - * Note that this method only prepares the response for file sending. The file is not sent - * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action. - * - * @param string $filePath the path of the file to be sent. - * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`. - * @param string $mimeType the MIME type of the content. If null, it will be guessed based on `$filePath` - * @return static the response object itself - */ - public function sendFile($filePath, $attachmentName = null, $mimeType = null) - { - if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { - $mimeType = 'application/octet-stream'; - } - if ($attachmentName === null) { - $attachmentName = basename($filePath); - } - $handle = fopen($filePath, 'rb'); - $this->sendStreamAsFile($handle, $attachmentName, $mimeType); - - return $this; - } - - /** - * Sends the specified content as a file to the browser. - * - * Note that this method only prepares the response for file sending. The file is not sent - * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action. - * - * @param string $content the content to be sent. The existing [[content]] will be discarded. - * @param string $attachmentName the file name shown to the user. - * @param string $mimeType the MIME type of the content. - * @return static the response object itself - * @throws HttpException if the requested range is not satisfiable - */ - public function sendContentAsFile($content, $attachmentName, $mimeType = 'application/octet-stream') - { - $headers = $this->getHeaders(); - $contentLength = StringHelper::byteLength($content); - $range = $this->getHttpRange($contentLength); - if ($range === false) { - $headers->set('Content-Range', "bytes */$contentLength"); - throw new HttpException(416, 'Requested range not satisfiable'); - } - - $headers->setDefault('Pragma', 'public') - ->setDefault('Accept-Ranges', 'bytes') - ->setDefault('Expires', '0') - ->setDefault('Content-Type', $mimeType) - ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->setDefault('Content-Transfer-Encoding', 'binary') - ->setDefault('Content-Length', StringHelper::byteLength($content)) - ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); - - list($begin, $end) = $range; - if ($begin !=0 || $end != $contentLength - 1) { - $this->setStatusCode(206); - $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); - $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1); - } else { - $this->setStatusCode(200); - $this->content = $content; - } - - $this->format = self::FORMAT_RAW; - - return $this; - } - - /** - * Sends the specified stream as a file to the browser. - * - * Note that this method only prepares the response for file sending. The file is not sent - * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action. - * - * @param resource $handle the handle of the stream to be sent. - * @param string $attachmentName the file name shown to the user. - * @param string $mimeType the MIME type of the stream content. - * @return static the response object itself - * @throws HttpException if the requested range cannot be satisfied. - */ - public function sendStreamAsFile($handle, $attachmentName, $mimeType = 'application/octet-stream') - { - $headers = $this->getHeaders(); - fseek($handle, 0, SEEK_END); - $fileSize = ftell($handle); - - $range = $this->getHttpRange($fileSize); - if ($range === false) { - $headers->set('Content-Range', "bytes */$fileSize"); - throw new HttpException(416, 'Requested range not satisfiable'); - } - - list($begin, $end) = $range; - if ($begin !=0 || $end != $fileSize - 1) { - $this->setStatusCode(206); - $headers->set('Content-Range', "bytes $begin-$end/$fileSize"); - } else { - $this->setStatusCode(200); - } - - $length = $end - $begin + 1; - - $headers->setDefault('Pragma', 'public') - ->setDefault('Accept-Ranges', 'bytes') - ->setDefault('Expires', '0') - ->setDefault('Content-Type', $mimeType) - ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->setDefault('Content-Transfer-Encoding', 'binary') - ->setDefault('Content-Length', $length) - ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); - $this->format = self::FORMAT_RAW; - $this->stream = [$handle, $begin, $end]; - - return $this; - } - - /** - * Determines the HTTP range given in the request. - * @param integer $fileSize the size of the file that will be used to validate the requested HTTP range. - * @return array|boolean the range (begin, end), or false if the range request is invalid. - */ - protected function getHttpRange($fileSize) - { - if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') { - return [0, $fileSize - 1]; - } - if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) { - return false; - } - if ($matches[1] === '') { - $start = $fileSize - $matches[2]; - $end = $fileSize - 1; - } elseif ($matches[2] !== '') { - $start = $matches[1]; - $end = $matches[2]; - if ($end >= $fileSize) { - $end = $fileSize - 1; - } - } else { - $start = $matches[1]; - $end = $fileSize - 1; - } - if ($start < 0 || $start > $end) { - return false; - } else { - return [$start, $end]; - } - } - - /** - * Sends existing file to a browser as a download using x-sendfile. - * - * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver - * that in turn processes the request, this way eliminating the need to perform tasks like reading the file - * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great - * increase in performance as the web application is allowed to terminate earlier while the webserver is - * handling the request. - * - * The request is sent to the server through a special non-standard HTTP-header. - * When the web server encounters the presence of such header it will discard all output and send the file - * specified by that header using web server internals including all optimizations like caching-headers. - * - * As this header directive is non-standard different directives exists for different web servers applications: - * - * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile) - * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file) - * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file) - * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile) - * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile) - * - * So for this method to work the X-SENDFILE option/module should be enabled by the web server and - * a proper xHeader should be sent. - * - * **Note** - * - * This option allows to download files that are not under web folders, and even files that are otherwise protected - * (deny from all) like `.htaccess`. - * - * **Side effects** - * - * If this option is disabled by the web server, when this method is called a download configuration dialog - * will open but the downloaded file will have 0 bytes. - * - * **Known issues** - * - * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show - * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site - * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header. - * - * **Example** - * - * ~~~ - * Yii::$app->response->xSendFile('/home/user/Pictures/picture1.jpg'); - * ~~~ - * - * @param string $filePath file name with full path - * @param string $mimeType the MIME type of the file. If null, it will be determined based on `$filePath`. - * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`. - * @param string $xHeader the name of the x-sendfile header. - * @return static the response object itself - */ - public function xSendFile($filePath, $attachmentName = null, $mimeType = null, $xHeader = 'X-Sendfile') - { - if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { - $mimeType = 'application/octet-stream'; - } - if ($attachmentName === null) { - $attachmentName = basename($filePath); - } - - $this->getHeaders() - ->setDefault($xHeader, $filePath) - ->setDefault('Content-Type', $mimeType) - ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); - - return $this; - } - - /** - * Redirects the browser to the specified URL. - * - * This method adds a "Location" header to the current response. Note that it does not send out - * the header until [[send()]] is called. In a controller action you may use this method as follows: - * - * ~~~ - * return Yii::$app->getResponse()->redirect($url); - * ~~~ - * - * In other places, if you want to send out the "Location" header immediately, you should use - * the following code: - * - * ~~~ - * Yii::$app->getResponse()->redirect($url)->send(); - * return; - * ~~~ - * - * In AJAX mode, this normally will not work as expected unless there are some - * client-side JavaScript code handling the redirection. To help achieve this goal, - * this method will send out a "X-Redirect" header instead of "Location". - * - * If you use the "yii" JavaScript module, it will handle the AJAX redirection as - * described above. Otherwise, you should write the following JavaScript code to - * handle the redirection: - * - * ~~~ - * $document.ajaxComplete(function (event, xhr, settings) { - * var url = xhr.getResponseHeader('X-Redirect'); - * if (url) { - * window.location = url; - * } - * }); - * ~~~ - * - * @param string|array $url the URL to be redirected to. This can be in one of the following formats: - * - * - a string representing a URL (e.g. "http://example.com") - * - a string representing a URL alias (e.g. "@example.com") - * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`). - * Note that the route is with respect to the whole application, instead of relative to a controller or module. - * [[Url::to()]] will be used to convert the array into a URL. - * - * Any relative URL will be converted into an absolute one by prepending it with the host info - * of the current request. - * - * @param integer $statusCode the HTTP status code. Defaults to 302. - * See - * for details about HTTP status code - * @return static the response object itself - */ - public function redirect($url, $statusCode = 302) - { - if (is_array($url) && isset($url[0])) { - // ensure the route is absolute - $url[0] = '/' . ltrim($url[0], '/'); - } - $url = Url::to($url); - if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { - $url = Yii::$app->getRequest()->getHostInfo() . $url; - } - - if (Yii::$app->getRequest()->getIsPjax()) { - $this->getHeaders()->set('X-Pjax-Url', $url); - } elseif (Yii::$app->getRequest()->getIsAjax()) { - $this->getHeaders()->set('X-Redirect', $url); - } else { - $this->getHeaders()->set('Location', $url); - } - $this->setStatusCode($statusCode); - - return $this; - } - - /** - * Refreshes the current page. - * The effect of this method call is the same as the user pressing the refresh button of his browser - * (without re-posting data). - * - * In a controller action you may use this method like this: - * - * ~~~ - * return Yii::$app->getResponse()->refresh(); - * ~~~ - * - * @param string $anchor the anchor that should be appended to the redirection URL. - * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. - * @return Response the response object itself - */ - public function refresh($anchor = '') - { - return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor); - } - - private $_cookies; - - /** - * Returns the cookie collection. - * Through the returned cookie collection, you add or remove cookies as follows, - * - * ~~~ - * // add a cookie - * $response->cookies->add(new Cookie([ - * 'name' => $name, - * 'value' => $value, - * ]); - * - * // remove a cookie - * $response->cookies->remove('name'); - * // alternatively - * unset($response->cookies['name']); - * ~~~ - * - * @return CookieCollection the cookie collection. - */ - public function getCookies() - { - if ($this->_cookies === null) { - $this->_cookies = new CookieCollection; - } - return $this->_cookies; - } - - /** - * @return boolean whether this response has a valid [[statusCode]]. - */ - public function getIsInvalid() - { - return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; - } - - /** - * @return boolean whether this response is informational - */ - public function getIsInformational() - { - return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; - } - - /** - * @return boolean whether this response is successful - */ - public function getIsSuccessful() - { - return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; - } - - /** - * @return boolean whether this response is a redirection - */ - public function getIsRedirection() - { - return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; - } - - /** - * @return boolean whether this response indicates a client error - */ - public function getIsClientError() - { - return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; - } - - /** - * @return boolean whether this response indicates a server error - */ - public function getIsServerError() - { - return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; - } - - /** - * @return boolean whether this response is OK - */ - public function getIsOk() - { - return $this->getStatusCode() == 200; - } - - /** - * @return boolean whether this response indicates the current request is forbidden - */ - public function getIsForbidden() - { - return $this->getStatusCode() == 403; - } - - /** - * @return boolean whether this response indicates the currently requested resource is not found - */ - public function getIsNotFound() - { - return $this->getStatusCode() == 404; - } - - /** - * @return boolean whether this response is empty - */ - public function getIsEmpty() - { - return in_array($this->getStatusCode(), [201, 204, 304]); - } - - /** - * Prepares for sending the response. - * The default implementation will convert [[data]] into [[content]] and set headers accordingly. - * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported - */ - protected function prepare() - { - if ($this->stream !== null || $this->data === null) { - return; - } - - if (isset($this->formatters[$this->format])) { - $formatter = $this->formatters[$this->format]; - if (!is_object($formatter)) { - $formatter = Yii::createObject($formatter); - } - if ($formatter instanceof ResponseFormatterInterface) { - $formatter->format($this); - } else { - throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); - } - } else { - switch ($this->format) { - case self::FORMAT_HTML: - $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); - $this->content = $this->data; - break; - case self::FORMAT_RAW: - $this->content = $this->data; - break; - case self::FORMAT_JSON: - $this->getHeaders()->set('Content-Type', 'application/json; charset=UTF-8'); - $this->content = Json::encode($this->data); - break; - case self::FORMAT_JSONP: - $this->getHeaders()->set('Content-Type', 'text/javascript; charset=' . $this->charset); - if (is_array($this->data) && isset($this->data['data'], $this->data['callback'])) { - $this->content = sprintf('%s(%s);', $this->data['callback'], Json::encode($this->data['data'])); - } else { - $this->content = ''; - Yii::warning("The 'jsonp' response requires that the data be an array consisting of both 'data' and 'callback' elements.", __METHOD__); - } - break; - case self::FORMAT_XML: - Yii::createObject(XmlResponseFormatter::className())->format($this); - break; - default: - throw new InvalidConfigException("Unsupported response format: {$this->format}"); - } - } - - if (is_array($this->content)) { - throw new InvalidParamException("Response content must not be an array."); - } elseif (is_object($this->content)) { - if (method_exists($this->content, '__toString')) { - $this->content = $this->content->__toString(); - } else { - throw new InvalidParamException("Response content must be a string or an object implementing __toString()."); - } - } - } + /** + * @event ResponseEvent an event that is triggered at the beginning of [[send()]]. + */ + const EVENT_BEFORE_SEND = 'beforeSend'; + /** + * @event ResponseEvent an event that is triggered at the end of [[send()]]. + */ + const EVENT_AFTER_SEND = 'afterSend'; + /** + * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]]. + * You may respond to this event to filter the response content before it is sent to the client. + */ + const EVENT_AFTER_PREPARE = 'afterPrepare'; + + const FORMAT_RAW = 'raw'; + const FORMAT_HTML = 'html'; + const FORMAT_JSON = 'json'; + const FORMAT_JSONP = 'jsonp'; + const FORMAT_XML = 'xml'; + + /** + * @var string the response format. This determines how to convert [[data]] into [[content]] + * when the latter is not set. By default, the following formats are supported: + * + * - [[FORMAT_RAW]]: the data will be treated as the response content without any conversion. + * No extra HTTP header will be added. + * - [[FORMAT_HTML]]: the data will be treated as the response content without any conversion. + * The "Content-Type" header will set as "text/html" if it is not set previously. + * - [[FORMAT_JSON]]: the data will be converted into JSON format, and the "Content-Type" + * header will be set as "application/json". + * - [[FORMAT_JSONP]]: the data will be converted into JSONP format, and the "Content-Type" + * header will be set as "text/javascript". Note that in this case `$data` must be an array + * with "data" and "callback" elements. The former refers to the actual data to be sent, + * while the latter refers to the name of the JavaScript callback. + * - [[FORMAT_XML]]: the data will be converted into XML format. Please refer to [[XmlResponseFormatter]] + * for more details. + * + * You may customize the formatting process or support additional formats by configuring [[formatters]]. + * @see formatters + */ + public $format = self::FORMAT_HTML; + /** + * @var array the formatters for converting data into the response content of the specified [[format]]. + * The array keys are the format names, and the array values are the corresponding configurations + * for creating the formatter objects. + * @see format + */ + public $formatters; + /** + * @var mixed the original response data. When this is not null, it will be converted into [[content]] + * according to [[format]] when the response is being sent out. + * @see content + */ + public $data; + /** + * @var string the response content. When [[data]] is not null, it will be converted into [[content]] + * according to [[format]] when the response is being sent out. + * @see data + */ + public $content; + /** + * @var resource|array the stream to be sent. This can be a stream handle or an array of stream handle, + * the begin position and the end position. Note that when this property is set, the [[data]] and [[content]] + * properties will be ignored by [[send()]]. + */ + public $stream; + /** + * @var string the charset of the text response. If not set, it will use + * the value of [[Application::charset]]. + */ + public $charset; + /** + * @var string the HTTP status description that comes together with the status code. + * @see httpStatuses + */ + public $statusText = 'OK'; + /** + * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`, + * or '1.1' if that is not available. + */ + public $version; + /** + * @var boolean whether the response has been sent. If this is true, calling [[send()]] will do nothing. + */ + public $isSent = false; + /** + * @var array list of HTTP status codes and the corresponding texts + */ + public static $httpStatuses = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 118 => 'Connection timed out', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 210 => 'Content Different', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 310 => 'Too many Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested range unsatisfiable', + 417 => 'Expectation failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable entity', + 423 => 'Locked', + 424 => 'Method failure', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 449 => 'Retry With', + 450 => 'Blocked by Windows Parental Controls', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway ou Proxy Error', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 507 => 'Insufficient storage', + 508 => 'Loop Detected', + 509 => 'Bandwidth Limit Exceeded', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + /** + * @var integer the HTTP status code to send with the response. + */ + private $_statusCode = 200; + /** + * @var HeaderCollection + */ + private $_headers; + + /** + * Initializes this component. + */ + public function init() + { + if ($this->version === null) { + if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === '1.0') { + $this->version = '1.0'; + } else { + $this->version = '1.1'; + } + } + if ($this->charset === null) { + $this->charset = Yii::$app->charset; + } + } + + /** + * @return integer the HTTP status code to send with the response. + */ + public function getStatusCode() + { + return $this->_statusCode; + } + + /** + * Sets the response status code. + * This method will set the corresponding status text if `$text` is null. + * @param integer $value the status code + * @param string $text the status text. If not set, it will be set automatically based on the status code. + * @throws InvalidParamException if the status code is invalid. + */ + public function setStatusCode($value, $text = null) + { + if ($value === null) { + $value = 200; + } + $this->_statusCode = (int) $value; + if ($this->getIsInvalid()) { + throw new InvalidParamException("The HTTP status code is invalid: $value"); + } + if ($text === null) { + $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : ''; + } else { + $this->statusText = $text; + } + } + + /** + * Returns the header collection. + * The header collection contains the currently registered HTTP headers. + * @return HeaderCollection the header collection + */ + public function getHeaders() + { + if ($this->_headers === null) { + $this->_headers = new HeaderCollection; + } + + return $this->_headers; + } + + /** + * Sends the response to the client. + */ + public function send() + { + if ($this->isSent) { + return; + } + $this->trigger(self::EVENT_BEFORE_SEND); + $this->prepare(); + $this->trigger(self::EVENT_AFTER_PREPARE); + $this->sendHeaders(); + $this->sendContent(); + $this->trigger(self::EVENT_AFTER_SEND); + $this->isSent = true; + } + + /** + * Clears the headers, cookies, content, status code of the response. + */ + public function clear() + { + $this->_headers = null; + $this->_cookies = null; + $this->_statusCode = 200; + $this->statusText = 'OK'; + $this->data = null; + $this->stream = null; + $this->content = null; + $this->isSent = false; + } + + /** + * Sends the response headers to the client + */ + protected function sendHeaders() + { + if (headers_sent()) { + return; + } + $statusCode = $this->getStatusCode(); + header("HTTP/{$this->version} $statusCode {$this->statusText}"); + if ($this->_headers) { + $headers = $this->getHeaders(); + foreach ($headers as $name => $values) { + $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); + foreach ($values as $value) { + header("$name: $value", false); + } + } + } + $this->sendCookies(); + } + + /** + * Sends the cookies to the client. + */ + protected function sendCookies() + { + if ($this->_cookies === null) { + return; + } + $request = Yii::$app->getRequest(); + if ($request->enableCookieValidation) { + $validationKey = $request->getCookieValidationKey(); + } + foreach ($this->getCookies() as $cookie) { + $value = $cookie->value; + if ($cookie->expire != 1 && isset($validationKey)) { + $value = Security::hashData(serialize($value), $validationKey); + } + setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); + } + $this->getCookies()->removeAll(); + } + + /** + * Sends the response content to the client + */ + protected function sendContent() + { + if ($this->stream === null) { + echo $this->content; + + return; + } + + set_time_limit(0); // Reset time limit for big files + $chunkSize = 8 * 1024 * 1024; // 8MB per chunk + + if (is_array($this->stream)) { + list ($handle, $begin, $end) = $this->stream; + fseek($handle, $begin); + while (!feof($handle) && ($pos = ftell($handle)) <= $end) { + if ($pos + $chunkSize > $end) { + $chunkSize = $end - $pos + 1; + } + echo fread($handle, $chunkSize); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } + fclose($handle); + } else { + while (!feof($this->stream)) { + echo fread($this->stream, $chunkSize); + flush(); + } + fclose($this->stream); + } + } + + /** + * Sends a file to the browser. + * + * Note that this method only prepares the response for file sending. The file is not sent + * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action. + * + * @param string $filePath the path of the file to be sent. + * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`. + * @param string $mimeType the MIME type of the content. If null, it will be guessed based on `$filePath` + * @return static the response object itself + */ + public function sendFile($filePath, $attachmentName = null, $mimeType = null) + { + if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { + $mimeType = 'application/octet-stream'; + } + if ($attachmentName === null) { + $attachmentName = basename($filePath); + } + $handle = fopen($filePath, 'rb'); + $this->sendStreamAsFile($handle, $attachmentName, $mimeType); + + return $this; + } + + /** + * Sends the specified content as a file to the browser. + * + * Note that this method only prepares the response for file sending. The file is not sent + * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action. + * + * @param string $content the content to be sent. The existing [[content]] will be discarded. + * @param string $attachmentName the file name shown to the user. + * @param string $mimeType the MIME type of the content. + * @return static the response object itself + * @throws HttpException if the requested range is not satisfiable + */ + public function sendContentAsFile($content, $attachmentName, $mimeType = 'application/octet-stream') + { + $headers = $this->getHeaders(); + $contentLength = StringHelper::byteLength($content); + $range = $this->getHttpRange($contentLength); + if ($range === false) { + $headers->set('Content-Range', "bytes */$contentLength"); + throw new HttpException(416, 'Requested range not satisfiable'); + } + + $headers->setDefault('Pragma', 'public') + ->setDefault('Accept-Ranges', 'bytes') + ->setDefault('Expires', '0') + ->setDefault('Content-Type', $mimeType) + ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->setDefault('Content-Transfer-Encoding', 'binary') + ->setDefault('Content-Length', StringHelper::byteLength($content)) + ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); + + list($begin, $end) = $range; + if ($begin !=0 || $end != $contentLength - 1) { + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); + $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1); + } else { + $this->setStatusCode(200); + $this->content = $content; + } + + $this->format = self::FORMAT_RAW; + + return $this; + } + + /** + * Sends the specified stream as a file to the browser. + * + * Note that this method only prepares the response for file sending. The file is not sent + * until [[send()]] is called explicitly or implicitly. The latter is done after you return from a controller action. + * + * @param resource $handle the handle of the stream to be sent. + * @param string $attachmentName the file name shown to the user. + * @param string $mimeType the MIME type of the stream content. + * @return static the response object itself + * @throws HttpException if the requested range cannot be satisfied. + */ + public function sendStreamAsFile($handle, $attachmentName, $mimeType = 'application/octet-stream') + { + $headers = $this->getHeaders(); + fseek($handle, 0, SEEK_END); + $fileSize = ftell($handle); + + $range = $this->getHttpRange($fileSize); + if ($range === false) { + $headers->set('Content-Range', "bytes */$fileSize"); + throw new HttpException(416, 'Requested range not satisfiable'); + } + + list($begin, $end) = $range; + if ($begin !=0 || $end != $fileSize - 1) { + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $begin-$end/$fileSize"); + } else { + $this->setStatusCode(200); + } + + $length = $end - $begin + 1; + + $headers->setDefault('Pragma', 'public') + ->setDefault('Accept-Ranges', 'bytes') + ->setDefault('Expires', '0') + ->setDefault('Content-Type', $mimeType) + ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->setDefault('Content-Transfer-Encoding', 'binary') + ->setDefault('Content-Length', $length) + ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); + $this->format = self::FORMAT_RAW; + $this->stream = [$handle, $begin, $end]; + + return $this; + } + + /** + * Determines the HTTP range given in the request. + * @param integer $fileSize the size of the file that will be used to validate the requested HTTP range. + * @return array|boolean the range (begin, end), or false if the range request is invalid. + */ + protected function getHttpRange($fileSize) + { + if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') { + return [0, $fileSize - 1]; + } + if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) { + return false; + } + if ($matches[1] === '') { + $start = $fileSize - $matches[2]; + $end = $fileSize - 1; + } elseif ($matches[2] !== '') { + $start = $matches[1]; + $end = $matches[2]; + if ($end >= $fileSize) { + $end = $fileSize - 1; + } + } else { + $start = $matches[1]; + $end = $fileSize - 1; + } + if ($start < 0 || $start > $end) { + return false; + } else { + return [$start, $end]; + } + } + + /** + * Sends existing file to a browser as a download using x-sendfile. + * + * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver + * that in turn processes the request, this way eliminating the need to perform tasks like reading the file + * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great + * increase in performance as the web application is allowed to terminate earlier while the webserver is + * handling the request. + * + * The request is sent to the server through a special non-standard HTTP-header. + * When the web server encounters the presence of such header it will discard all output and send the file + * specified by that header using web server internals including all optimizations like caching-headers. + * + * As this header directive is non-standard different directives exists for different web servers applications: + * + * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile) + * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file) + * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file) + * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile) + * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile) + * + * So for this method to work the X-SENDFILE option/module should be enabled by the web server and + * a proper xHeader should be sent. + * + * **Note** + * + * This option allows to download files that are not under web folders, and even files that are otherwise protected + * (deny from all) like `.htaccess`. + * + * **Side effects** + * + * If this option is disabled by the web server, when this method is called a download configuration dialog + * will open but the downloaded file will have 0 bytes. + * + * **Known issues** + * + * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show + * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site + * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header. + * + * **Example** + * + * ~~~ + * Yii::$app->response->xSendFile('/home/user/Pictures/picture1.jpg'); + * ~~~ + * + * @param string $filePath file name with full path + * @param string $mimeType the MIME type of the file. If null, it will be determined based on `$filePath`. + * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`. + * @param string $xHeader the name of the x-sendfile header. + * @return static the response object itself + */ + public function xSendFile($filePath, $attachmentName = null, $mimeType = null, $xHeader = 'X-Sendfile') + { + if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { + $mimeType = 'application/octet-stream'; + } + if ($attachmentName === null) { + $attachmentName = basename($filePath); + } + + $this->getHeaders() + ->setDefault($xHeader, $filePath) + ->setDefault('Content-Type', $mimeType) + ->setDefault('Content-Disposition', "attachment; filename=\"$attachmentName\""); + + return $this; + } + + /** + * Redirects the browser to the specified URL. + * + * This method adds a "Location" header to the current response. Note that it does not send out + * the header until [[send()]] is called. In a controller action you may use this method as follows: + * + * ~~~ + * return Yii::$app->getResponse()->redirect($url); + * ~~~ + * + * In other places, if you want to send out the "Location" header immediately, you should use + * the following code: + * + * ~~~ + * Yii::$app->getResponse()->redirect($url)->send(); + * return; + * ~~~ + * + * In AJAX mode, this normally will not work as expected unless there are some + * client-side JavaScript code handling the redirection. To help achieve this goal, + * this method will send out a "X-Redirect" header instead of "Location". + * + * If you use the "yii" JavaScript module, it will handle the AJAX redirection as + * described above. Otherwise, you should write the following JavaScript code to + * handle the redirection: + * + * ~~~ + * $document.ajaxComplete(function (event, xhr, settings) { + * var url = xhr.getResponseHeader('X-Redirect'); + * if (url) { + * window.location = url; + * } + * }); + * ~~~ + * + * @param string|array $url the URL to be redirected to. This can be in one of the following formats: + * + * - a string representing a URL (e.g. "http://example.com") + * - a string representing a URL alias (e.g. "@example.com") + * - an array in the format of `[$route, ...name-value pairs...]` (e.g. `['site/index', 'ref' => 1]`). + * Note that the route is with respect to the whole application, instead of relative to a controller or module. + * [[Url::to()]] will be used to convert the array into a URL. + * + * Any relative URL will be converted into an absolute one by prepending it with the host info + * of the current request. + * + * @param integer $statusCode the HTTP status code. Defaults to 302. + * See + * for details about HTTP status code + * @return static the response object itself + */ + public function redirect($url, $statusCode = 302) + { + if (is_array($url) && isset($url[0])) { + // ensure the route is absolute + $url[0] = '/' . ltrim($url[0], '/'); + } + $url = Url::to($url); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + + if (Yii::$app->getRequest()->getIsPjax()) { + $this->getHeaders()->set('X-Pjax-Url', $url); + } elseif (Yii::$app->getRequest()->getIsAjax()) { + $this->getHeaders()->set('X-Redirect', $url); + } else { + $this->getHeaders()->set('Location', $url); + } + $this->setStatusCode($statusCode); + + return $this; + } + + /** + * Refreshes the current page. + * The effect of this method call is the same as the user pressing the refresh button of his browser + * (without re-posting data). + * + * In a controller action you may use this method like this: + * + * ~~~ + * return Yii::$app->getResponse()->refresh(); + * ~~~ + * + * @param string $anchor the anchor that should be appended to the redirection URL. + * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. + * @return Response the response object itself + */ + public function refresh($anchor = '') + { + return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor); + } + + private $_cookies; + + /** + * Returns the cookie collection. + * Through the returned cookie collection, you add or remove cookies as follows, + * + * ~~~ + * // add a cookie + * $response->cookies->add(new Cookie([ + * 'name' => $name, + * 'value' => $value, + * ]); + * + * // remove a cookie + * $response->cookies->remove('name'); + * // alternatively + * unset($response->cookies['name']); + * ~~~ + * + * @return CookieCollection the cookie collection. + */ + public function getCookies() + { + if ($this->_cookies === null) { + $this->_cookies = new CookieCollection; + } + + return $this->_cookies; + } + + /** + * @return boolean whether this response has a valid [[statusCode]]. + */ + public function getIsInvalid() + { + return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; + } + + /** + * @return boolean whether this response is informational + */ + public function getIsInformational() + { + return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; + } + + /** + * @return boolean whether this response is successful + */ + public function getIsSuccessful() + { + return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; + } + + /** + * @return boolean whether this response is a redirection + */ + public function getIsRedirection() + { + return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; + } + + /** + * @return boolean whether this response indicates a client error + */ + public function getIsClientError() + { + return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; + } + + /** + * @return boolean whether this response indicates a server error + */ + public function getIsServerError() + { + return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; + } + + /** + * @return boolean whether this response is OK + */ + public function getIsOk() + { + return $this->getStatusCode() == 200; + } + + /** + * @return boolean whether this response indicates the current request is forbidden + */ + public function getIsForbidden() + { + return $this->getStatusCode() == 403; + } + + /** + * @return boolean whether this response indicates the currently requested resource is not found + */ + public function getIsNotFound() + { + return $this->getStatusCode() == 404; + } + + /** + * @return boolean whether this response is empty + */ + public function getIsEmpty() + { + return in_array($this->getStatusCode(), [201, 204, 304]); + } + + /** + * Prepares for sending the response. + * The default implementation will convert [[data]] into [[content]] and set headers accordingly. + * @throws InvalidConfigException if the formatter for the specified format is invalid or [[format]] is not supported + */ + protected function prepare() + { + if ($this->stream !== null || $this->data === null) { + return; + } + + if (isset($this->formatters[$this->format])) { + $formatter = $this->formatters[$this->format]; + if (!is_object($formatter)) { + $formatter = Yii::createObject($formatter); + } + if ($formatter instanceof ResponseFormatterInterface) { + $formatter->format($this); + } else { + throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); + } + } else { + switch ($this->format) { + case self::FORMAT_HTML: + $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); + $this->content = $this->data; + break; + case self::FORMAT_RAW: + $this->content = $this->data; + break; + case self::FORMAT_JSON: + $this->getHeaders()->set('Content-Type', 'application/json; charset=UTF-8'); + $this->content = Json::encode($this->data); + break; + case self::FORMAT_JSONP: + $this->getHeaders()->set('Content-Type', 'text/javascript; charset=' . $this->charset); + if (is_array($this->data) && isset($this->data['data'], $this->data['callback'])) { + $this->content = sprintf('%s(%s);', $this->data['callback'], Json::encode($this->data['data'])); + } else { + $this->content = ''; + Yii::warning("The 'jsonp' response requires that the data be an array consisting of both 'data' and 'callback' elements.", __METHOD__); + } + break; + case self::FORMAT_XML: + Yii::createObject(XmlResponseFormatter::className())->format($this); + break; + default: + throw new InvalidConfigException("Unsupported response format: {$this->format}"); + } + } + + if (is_array($this->content)) { + throw new InvalidParamException("Response content must not be an array."); + } elseif (is_object($this->content)) { + if (method_exists($this->content, '__toString')) { + $this->content = $this->content->__toString(); + } else { + throw new InvalidParamException("Response content must be a string or an object implementing __toString()."); + } + } + } } diff --git a/framework/web/ResponseFormatterInterface.php b/framework/web/ResponseFormatterInterface.php index 689ee1e790d..585c755a8dd 100644 --- a/framework/web/ResponseFormatterInterface.php +++ b/framework/web/ResponseFormatterInterface.php @@ -15,9 +15,9 @@ */ interface ResponseFormatterInterface { - /** - * Formats the specified response. - * @param Response $response the response to be formatted. - */ - public function format($response); + /** + * Formats the specified response. + * @param Response $response the response to be formatted. + */ + public function format($response); } diff --git a/framework/web/Session.php b/framework/web/Session.php index d08847f4c13..fac6bdb4929 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -74,690 +74,700 @@ */ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Countable { - /** - * @var string the name of the session variable that stores the flash message data. - */ - public $flashParam = '__flash'; - /** - * @var \SessionHandlerInterface|array an object implementing the SessionHandlerInterface or a configuration array. If set, will be used to provide persistency instead of build-in methods. - */ - public $handler; - /** - * @var array parameter-value pairs to override default session cookie parameters that are used for session_set_cookie_params() function - * Array may have the following possible keys: 'lifetime', 'path', 'domain', 'secure', 'httpOnly' - * @see http://www.php.net/manual/en/function.session-set-cookie-params.php - */ - private $_cookieParams = ['httpOnly' => true]; - - /** - * Initializes the application component. - * This method is required by IApplicationComponent and is invoked by application. - */ - public function init() - { - parent::init(); - register_shutdown_function([$this, 'close']); - } - - /** - * Returns a value indicating whether to use custom session storage. - * This method should be overridden to return true by child classes that implement custom session storage. - * To implement custom session storage, override these methods: [[openSession()]], [[closeSession()]], - * [[readSession()]], [[writeSession()]], [[destroySession()]] and [[gcSession()]]. - * @return boolean whether to use custom storage. - */ - public function getUseCustomStorage() - { - return false; - } - - /** - * Starts the session. - */ - public function open() - { - if ($this->getIsActive()) { - return; - } - - $this->registerSessionHandler(); - - $this->setCookieParamsInternal(); - - @session_start(); - - if ($this->getIsActive()) { - Yii::info('Session started', __METHOD__); - $this->updateFlashCounters(); - } else { - $error = error_get_last(); - $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::error($message, __METHOD__); - } - } - - /** - * Registers session handler. - * @throws \yii\base\InvalidConfigException - */ - protected function registerSessionHandler() - { - if ($this->handler !== null) { - if (!is_object($this->handler)) { - $this->handler = Yii::createObject($this->handler); - } - if (!$this->handler instanceof \SessionHandlerInterface) { - throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.'); - } - @session_set_save_handler($this->handler, false); - } elseif ($this->getUseCustomStorage()) { - @session_set_save_handler( - [$this, 'openSession'], - [$this, 'closeSession'], - [$this, 'readSession'], - [$this, 'writeSession'], - [$this, 'destroySession'], - [$this, 'gcSession'] - ); - } - } - - /** - * Ends the current session and store session data. - */ - public function close() - { - if ($this->getIsActive()) { - @session_write_close(); - } - } - - /** - * Frees all session variables and destroys all data registered to a session. - */ - public function destroy() - { - if ($this->getIsActive()) { - @session_unset(); - @session_destroy(); - } - } - - /** - * @return boolean whether the session has started - */ - public function getIsActive() - { - return session_status() == PHP_SESSION_ACTIVE; - } - - private $_hasSessionId; - - /** - * Returns a value indicating whether the current request has sent the session ID. - * The default implementation will check cookie and $_GET using the session name. - * If you send session ID via other ways, you may need to override this method - * or call [[setHasSessionId()]] to explicitly set whether the session ID is sent. - * @return boolean whether the current request has sent the session ID. - */ - public function getHasSessionId() - { - if ($this->_hasSessionId === null) { - $name = $this->getName(); - $request = Yii::$app->getRequest(); - if (ini_get('session.use_cookies') && !empty($_COOKIE[$name])) { - $this->_hasSessionId = true; - } elseif (!ini_get('use_only_cookies') && ini_get('use_trans_sid')) { - $this->_hasSessionId = $request->get($name) !== null; - } else { - $this->_hasSessionId = false; - } - } - return $this->_hasSessionId; - } - - /** - * Sets the value indicating whether the current request has sent the session ID. - * This method is provided so that you can override the default way of determining - * whether the session ID is sent. - * @param boolean $value whether the current request has sent the session ID. - */ - public function setHasSessionId($value) - { - $this->_hasSessionId = $value; - } - - /** - * @return string the current session ID - */ - public function getId() - { - return session_id(); - } - - /** - * @param string $value the session ID for the current session - */ - public function setId($value) - { - session_id($value); - } - - /** - * Updates the current session ID with a newly generated one . - * Please refer to for more details. - * @param boolean $deleteOldSession Whether to delete the old associated session file or not. - */ - public function regenerateID($deleteOldSession = false) - { - // add @ to inhibit possible warning due to race condition - // https://github.com/yiisoft/yii2/pull/1812 - @session_regenerate_id($deleteOldSession); - } - - /** - * @return string the current session name - */ - public function getName() - { - return session_name(); - } - - /** - * @param string $value the session name for the current session, must be an alphanumeric string. - * It defaults to "PHPSESSID". - */ - public function setName($value) - { - session_name($value); - } - - /** - * @return string the current session save path, defaults to '/tmp'. - */ - public function getSavePath() - { - return session_save_path(); - } - - /** - * @param string $value the current session save path. This can be either a directory name or a path alias. - * @throws InvalidParamException if the path is not a valid directory - */ - public function setSavePath($value) - { - $path = Yii::getAlias($value); - if (is_dir($path)) { - session_save_path($path); - } else { - throw new InvalidParamException("Session save path is not a valid directory: $value"); - } - } - - /** - * @return array the session cookie parameters. - * @see http://us2.php.net/manual/en/function.session-get-cookie-params.php - */ - public function getCookieParams() - { - $params = session_get_cookie_params(); - if (isset($params['httponly'])) { - $params['httpOnly'] = $params['httponly']; - unset($params['httponly']); - } - return array_merge($params, $this->_cookieParams); - } - - /** - * Sets the session cookie parameters. - * The cookie parameters passed to this method will be merged with the result - * of `session_get_cookie_params()`. - * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httpOnly`. - * @throws InvalidParamException if the parameters are incomplete. - * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php - */ - public function setCookieParams(array $value) - { - $this->_cookieParams = $value; - } - - /** - * Sets the session cookie parameters. - * This method is called by [[open()]] when it is about to open the session. - * @throws InvalidParamException if the parameters are incomplete. - * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php - */ - private function setCookieParamsInternal() - { - $data = $this->getCookieParams(); - extract($data); - if (isset($lifetime, $path, $domain, $secure, $httpOnly)) { - session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly); - } else { - throw new InvalidParamException('Please make sure cookieParams contains these elements: lifetime, path, domain, secure and httpOnly.'); - } - } - - /** - * Returns the value indicating whether cookies should be used to store session IDs. - * @return boolean|null the value indicating whether cookies should be used to store session IDs. - * @see setUseCookies() - */ - public function getUseCookies() - { - if (ini_get('session.use_cookies') === '0') { - return false; - } elseif (ini_get('session.use_only_cookies') === '1') { - return true; - } else { - return null; - } - } - - /** - * Sets the value indicating whether cookies should be used to store session IDs. - * Three states are possible: - * - * - true: cookies and only cookies will be used to store session IDs. - * - false: cookies will not be used to store session IDs. - * - null: if possible, cookies will be used to store session IDs; if not, other mechanisms will be used (e.g. GET parameter) - * - * @param boolean|null $value the value indicating whether cookies should be used to store session IDs. - */ - public function setUseCookies($value) - { - if ($value === false) { - ini_set('session.use_cookies', '0'); - ini_set('session.use_only_cookies', '0'); - } elseif ($value === true) { - ini_set('session.use_cookies', '1'); - ini_set('session.use_only_cookies', '1'); - } else { - ini_set('session.use_cookies', '1'); - ini_set('session.use_only_cookies', '0'); - } - } - - /** - * @return float the probability (percentage) that the GC (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. - */ - public function getGCProbability() - { - return (float)(ini_get('session.gc_probability') / ini_get('session.gc_divisor') * 100); - } - - /** - * @param float $value the probability (percentage) that the GC (garbage collection) process is started on every session initialization. - * @throws InvalidParamException if the value is not between 0 and 100. - */ - public function setGCProbability($value) - { - if ($value >= 0 && $value <= 100) { - // percent * 21474837 / 2147483647 ≈ percent * 0.01 - ini_set('session.gc_probability', floor($value * 21474836.47)); - ini_set('session.gc_divisor', 2147483647); - } else { - throw new InvalidParamException('GCProbability must be a value between 0 and 100.'); - } - } - - /** - * @return boolean whether transparent sid support is enabled or not, defaults to false. - */ - public function getUseTransparentSessionID() - { - return ini_get('session.use_trans_sid') == 1; - } - - /** - * @param boolean $value whether transparent sid support is enabled or not. - */ - public function setUseTransparentSessionID($value) - { - ini_set('session.use_trans_sid', $value ? '1' : '0'); - } - - /** - * @return integer the number of seconds after which data will be seen as 'garbage' and cleaned up. - * The default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini). - */ - public function getTimeout() - { - return (int)ini_get('session.gc_maxlifetime'); - } - - /** - * @param integer $value the number of seconds after which data will be seen as 'garbage' and cleaned up - */ - public function setTimeout($value) - { - ini_set('session.gc_maxlifetime', $value); - } - - /** - * Session open handler. - * This method should be overridden if [[useCustomStorage()]] returns true. - * Do not call this method directly. - * @param string $savePath session save path - * @param string $sessionName session name - * @return boolean whether session is opened successfully - */ - public function openSession($savePath, $sessionName) - { - return true; - } - - /** - * Session close handler. - * This method should be overridden if [[useCustomStorage()]] returns true. - * Do not call this method directly. - * @return boolean whether session is closed successfully - */ - public function closeSession() - { - return true; - } - - /** - * Session read handler. - * This method should be overridden if [[useCustomStorage()]] returns true. - * Do not call this method directly. - * @param string $id session ID - * @return string the session data - */ - public function readSession($id) - { - return ''; - } - - /** - * Session write handler. - * This method should be overridden if [[useCustomStorage()]] returns true. - * Do not call this method directly. - * @param string $id session ID - * @param string $data session data - * @return boolean whether session write is successful - */ - public function writeSession($id, $data) - { - return true; - } - - /** - * Session destroy handler. - * This method should be overridden if [[useCustomStorage()]] returns true. - * Do not call this method directly. - * @param string $id session ID - * @return boolean whether session is destroyed successfully - */ - public function destroySession($id) - { - return true; - } - - /** - * Session GC (garbage collection) handler. - * This method should be overridden if [[useCustomStorage()]] returns true. - * Do not call this method directly. - * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. - * @return boolean whether session is GCed successfully - */ - public function gcSession($maxLifetime) - { - return true; - } - - /** - * Returns an iterator for traversing the session variables. - * This method is required by the interface IteratorAggregate. - * @return SessionIterator an iterator for traversing the session variables. - */ - public function getIterator() - { - return new SessionIterator; - } - - /** - * Returns the number of items in the session. - * @return integer the number of session variables - */ - public function getCount() - { - return count($_SESSION); - } - - /** - * Returns the number of items in the session. - * This method is required by Countable interface. - * @return integer number of items in the session. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the session variable value with the session variable name. - * If the session variable does not exist, the `$defaultValue` will be returned. - * @param string $key the session variable name - * @param mixed $defaultValue the default value to be returned when the session variable does not exist. - * @return mixed the session variable value, or $defaultValue if the session variable does not exist. - */ - public function get($key, $defaultValue = null) - { - $this->open(); - return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; - } - - /** - * Adds a session variable. - * If the specified name already exists, the old value will be overwritten. - * @param string $key session variable name - * @param mixed $value session variable value - */ - public function set($key, $value) - { - $this->open(); - $_SESSION[$key] = $value; - } - - /** - * Removes a session variable. - * @param string $key the name of the session variable to be removed - * @return mixed the removed value, null if no such session variable. - */ - public function remove($key) - { - $this->open(); - if (isset($_SESSION[$key])) { - $value = $_SESSION[$key]; - unset($_SESSION[$key]); - return $value; - } else { - return null; - } - } - - /** - * Removes all session variables - */ - public function removeAll() - { - $this->open(); - foreach (array_keys($_SESSION) as $key) { - unset($_SESSION[$key]); - } - } - - /** - * @param mixed $key session variable name - * @return boolean whether there is the named session variable - */ - public function has($key) - { - $this->open(); - return isset($_SESSION[$key]); - } - - /** - * Updates the counters for flash messages and removes outdated flash messages. - * This method should only be called once in [[init()]]. - */ - protected function updateFlashCounters() - { - $counters = $this->get($this->flashParam, []); - if (is_array($counters)) { - foreach ($counters as $key => $count) { - if ($count) { - unset($counters[$key], $_SESSION[$key]); - } else { - $counters[$key]++; - } - } - $_SESSION[$this->flashParam] = $counters; - } else { - // fix the unexpected problem that flashParam doesn't return an array - unset($_SESSION[$this->flashParam]); - } - } - - /** - * Returns a flash message. - * A flash message is available only in the current request and the next request. - * @param string $key the key identifying the flash message - * @param mixed $defaultValue value to be returned if the flash message does not exist. - * @param boolean $delete whether to delete this flash message right after this method is called. - * If false, the flash message will be automatically deleted after the next request. - * @return mixed the flash message - */ - public function getFlash($key, $defaultValue = null, $delete = false) - { - $counters = $this->get($this->flashParam, []); - if (isset($counters[$key])) { - $value = $this->get($key, $defaultValue); - if ($delete) { - $this->removeFlash($key); - } - return $value; - } else { - return $defaultValue; - } - } - - /** - * Returns all flash messages. - * @return array flash messages (key => message). - */ - public function getAllFlashes() - { - $counters = $this->get($this->flashParam, []); - $flashes = []; - foreach (array_keys($counters) as $key) { - if (isset($_SESSION[$key])) { - $flashes[$key] = $_SESSION[$key]; - } - } - return $flashes; - } - - /** - * Stores a flash message. - * A flash message is available only in the current request and the next request. - * @param string $key the key identifying the flash message. Note that flash messages - * and normal session variables share the same name space. If you have a normal - * session variable using the same name, its value will be overwritten by this method. - * @param mixed $value flash message - */ - public function setFlash($key, $value = true) - { - $counters = $this->get($this->flashParam, []); - $counters[$key] = 0; - $_SESSION[$key] = $value; - $_SESSION[$this->flashParam] = $counters; - } - - /** - * Removes a flash message. - * Note that flash messages will be automatically removed after the next request. - * @param string $key the key identifying the flash message. Note that flash messages - * and normal session variables share the same name space. If you have a normal - * session variable using the same name, it will be removed by this method. - * @return mixed the removed flash message. Null if the flash message does not exist. - */ - public function removeFlash($key) - { - $counters = $this->get($this->flashParam, []); - $value = isset($_SESSION[$key], $counters[$key]) ? $_SESSION[$key] : null; - unset($counters[$key], $_SESSION[$key]); - $_SESSION[$this->flashParam] = $counters; - return $value; - } - - /** - * Removes all flash messages. - * Note that flash messages and normal session variables share the same name space. - * If you have a normal session variable using the same name, it will be removed - * by this method. - */ - public function removeAllFlashes() - { - $counters = $this->get($this->flashParam, []); - foreach (array_keys($counters) as $key) { - unset($_SESSION[$key]); - } - unset($_SESSION[$this->flashParam]); - } - - /** - * Returns a value indicating whether there is a flash message associated with the specified key. - * @param string $key key identifying the flash message - * @return boolean whether the specified flash message exists - */ - public function hasFlash($key) - { - return $this->getFlash($key) !== null; - } - - /** - * This method is required by the interface ArrayAccess. - * @param mixed $offset the offset to check on - * @return boolean - */ - public function offsetExists($offset) - { - $this->open(); - return isset($_SESSION[$offset]); - } - - /** - * This method is required by the interface ArrayAccess. - * @param integer $offset the offset to retrieve element. - * @return mixed the element at the offset, null if no element is found at the offset - */ - public function offsetGet($offset) - { - $this->open(); - return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; - } - - /** - * This method is required by the interface ArrayAccess. - * @param integer $offset the offset to set element - * @param mixed $item the element value - */ - public function offsetSet($offset, $item) - { - $this->open(); - $_SESSION[$offset] = $item; - } - - /** - * This method is required by the interface ArrayAccess. - * @param mixed $offset the offset to unset element - */ - public function offsetUnset($offset) - { - $this->open(); - unset($_SESSION[$offset]); - } + /** + * @var string the name of the session variable that stores the flash message data. + */ + public $flashParam = '__flash'; + /** + * @var \SessionHandlerInterface|array an object implementing the SessionHandlerInterface or a configuration array. If set, will be used to provide persistency instead of build-in methods. + */ + public $handler; + /** + * @var array parameter-value pairs to override default session cookie parameters that are used for session_set_cookie_params() function + * Array may have the following possible keys: 'lifetime', 'path', 'domain', 'secure', 'httpOnly' + * @see http://www.php.net/manual/en/function.session-set-cookie-params.php + */ + private $_cookieParams = ['httpOnly' => true]; + + /** + * Initializes the application component. + * This method is required by IApplicationComponent and is invoked by application. + */ + public function init() + { + parent::init(); + register_shutdown_function([$this, 'close']); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method should be overridden to return true by child classes that implement custom session storage. + * To implement custom session storage, override these methods: [[openSession()]], [[closeSession()]], + * [[readSession()]], [[writeSession()]], [[destroySession()]] and [[gcSession()]]. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return false; + } + + /** + * Starts the session. + */ + public function open() + { + if ($this->getIsActive()) { + return; + } + + $this->registerSessionHandler(); + + $this->setCookieParamsInternal(); + + @session_start(); + + if ($this->getIsActive()) { + Yii::info('Session started', __METHOD__); + $this->updateFlashCounters(); + } else { + $error = error_get_last(); + $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; + Yii::error($message, __METHOD__); + } + } + + /** + * Registers session handler. + * @throws \yii\base\InvalidConfigException + */ + protected function registerSessionHandler() + { + if ($this->handler !== null) { + if (!is_object($this->handler)) { + $this->handler = Yii::createObject($this->handler); + } + if (!$this->handler instanceof \SessionHandlerInterface) { + throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.'); + } + @session_set_save_handler($this->handler, false); + } elseif ($this->getUseCustomStorage()) { + @session_set_save_handler( + [$this, 'openSession'], + [$this, 'closeSession'], + [$this, 'readSession'], + [$this, 'writeSession'], + [$this, 'destroySession'], + [$this, 'gcSession'] + ); + } + } + + /** + * Ends the current session and store session data. + */ + public function close() + { + if ($this->getIsActive()) { + @session_write_close(); + } + } + + /** + * Frees all session variables and destroys all data registered to a session. + */ + public function destroy() + { + if ($this->getIsActive()) { + @session_unset(); + @session_destroy(); + } + } + + /** + * @return boolean whether the session has started + */ + public function getIsActive() + { + return session_status() == PHP_SESSION_ACTIVE; + } + + private $_hasSessionId; + + /** + * Returns a value indicating whether the current request has sent the session ID. + * The default implementation will check cookie and $_GET using the session name. + * If you send session ID via other ways, you may need to override this method + * or call [[setHasSessionId()]] to explicitly set whether the session ID is sent. + * @return boolean whether the current request has sent the session ID. + */ + public function getHasSessionId() + { + if ($this->_hasSessionId === null) { + $name = $this->getName(); + $request = Yii::$app->getRequest(); + if (ini_get('session.use_cookies') && !empty($_COOKIE[$name])) { + $this->_hasSessionId = true; + } elseif (!ini_get('use_only_cookies') && ini_get('use_trans_sid')) { + $this->_hasSessionId = $request->get($name) !== null; + } else { + $this->_hasSessionId = false; + } + } + + return $this->_hasSessionId; + } + + /** + * Sets the value indicating whether the current request has sent the session ID. + * This method is provided so that you can override the default way of determining + * whether the session ID is sent. + * @param boolean $value whether the current request has sent the session ID. + */ + public function setHasSessionId($value) + { + $this->_hasSessionId = $value; + } + + /** + * @return string the current session ID + */ + public function getId() + { + return session_id(); + } + + /** + * @param string $value the session ID for the current session + */ + public function setId($value) + { + session_id($value); + } + + /** + * Updates the current session ID with a newly generated one . + * Please refer to for more details. + * @param boolean $deleteOldSession Whether to delete the old associated session file or not. + */ + public function regenerateID($deleteOldSession = false) + { + // add @ to inhibit possible warning due to race condition + // https://github.com/yiisoft/yii2/pull/1812 + @session_regenerate_id($deleteOldSession); + } + + /** + * @return string the current session name + */ + public function getName() + { + return session_name(); + } + + /** + * @param string $value the session name for the current session, must be an alphanumeric string. + * It defaults to "PHPSESSID". + */ + public function setName($value) + { + session_name($value); + } + + /** + * @return string the current session save path, defaults to '/tmp'. + */ + public function getSavePath() + { + return session_save_path(); + } + + /** + * @param string $value the current session save path. This can be either a directory name or a path alias. + * @throws InvalidParamException if the path is not a valid directory + */ + public function setSavePath($value) + { + $path = Yii::getAlias($value); + if (is_dir($path)) { + session_save_path($path); + } else { + throw new InvalidParamException("Session save path is not a valid directory: $value"); + } + } + + /** + * @return array the session cookie parameters. + * @see http://us2.php.net/manual/en/function.session-get-cookie-params.php + */ + public function getCookieParams() + { + $params = session_get_cookie_params(); + if (isset($params['httponly'])) { + $params['httpOnly'] = $params['httponly']; + unset($params['httponly']); + } + + return array_merge($params, $this->_cookieParams); + } + + /** + * Sets the session cookie parameters. + * The cookie parameters passed to this method will be merged with the result + * of `session_get_cookie_params()`. + * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httpOnly`. + * @throws InvalidParamException if the parameters are incomplete. + * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php + */ + public function setCookieParams(array $value) + { + $this->_cookieParams = $value; + } + + /** + * Sets the session cookie parameters. + * This method is called by [[open()]] when it is about to open the session. + * @throws InvalidParamException if the parameters are incomplete. + * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php + */ + private function setCookieParamsInternal() + { + $data = $this->getCookieParams(); + extract($data); + if (isset($lifetime, $path, $domain, $secure, $httpOnly)) { + session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly); + } else { + throw new InvalidParamException('Please make sure cookieParams contains these elements: lifetime, path, domain, secure and httpOnly.'); + } + } + + /** + * Returns the value indicating whether cookies should be used to store session IDs. + * @return boolean|null the value indicating whether cookies should be used to store session IDs. + * @see setUseCookies() + */ + public function getUseCookies() + { + if (ini_get('session.use_cookies') === '0') { + return false; + } elseif (ini_get('session.use_only_cookies') === '1') { + return true; + } else { + return null; + } + } + + /** + * Sets the value indicating whether cookies should be used to store session IDs. + * Three states are possible: + * + * - true: cookies and only cookies will be used to store session IDs. + * - false: cookies will not be used to store session IDs. + * - null: if possible, cookies will be used to store session IDs; if not, other mechanisms will be used (e.g. GET parameter) + * + * @param boolean|null $value the value indicating whether cookies should be used to store session IDs. + */ + public function setUseCookies($value) + { + if ($value === false) { + ini_set('session.use_cookies', '0'); + ini_set('session.use_only_cookies', '0'); + } elseif ($value === true) { + ini_set('session.use_cookies', '1'); + ini_set('session.use_only_cookies', '1'); + } else { + ini_set('session.use_cookies', '1'); + ini_set('session.use_only_cookies', '0'); + } + } + + /** + * @return float the probability (percentage) that the GC (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. + */ + public function getGCProbability() + { + return (float) (ini_get('session.gc_probability') / ini_get('session.gc_divisor') * 100); + } + + /** + * @param float $value the probability (percentage) that the GC (garbage collection) process is started on every session initialization. + * @throws InvalidParamException if the value is not between 0 and 100. + */ + public function setGCProbability($value) + { + if ($value >= 0 && $value <= 100) { + // percent * 21474837 / 2147483647 ≈ percent * 0.01 + ini_set('session.gc_probability', floor($value * 21474836.47)); + ini_set('session.gc_divisor', 2147483647); + } else { + throw new InvalidParamException('GCProbability must be a value between 0 and 100.'); + } + } + + /** + * @return boolean whether transparent sid support is enabled or not, defaults to false. + */ + public function getUseTransparentSessionID() + { + return ini_get('session.use_trans_sid') == 1; + } + + /** + * @param boolean $value whether transparent sid support is enabled or not. + */ + public function setUseTransparentSessionID($value) + { + ini_set('session.use_trans_sid', $value ? '1' : '0'); + } + + /** + * @return integer the number of seconds after which data will be seen as 'garbage' and cleaned up. + * The default value is 1440 seconds (or the value of "session.gc_maxlifetime" set in php.ini). + */ + public function getTimeout() + { + return (int) ini_get('session.gc_maxlifetime'); + } + + /** + * @param integer $value the number of seconds after which data will be seen as 'garbage' and cleaned up + */ + public function setTimeout($value) + { + ini_set('session.gc_maxlifetime', $value); + } + + /** + * Session open handler. + * This method should be overridden if [[useCustomStorage()]] returns true. + * Do not call this method directly. + * @param string $savePath session save path + * @param string $sessionName session name + * @return boolean whether session is opened successfully + */ + public function openSession($savePath, $sessionName) + { + return true; + } + + /** + * Session close handler. + * This method should be overridden if [[useCustomStorage()]] returns true. + * Do not call this method directly. + * @return boolean whether session is closed successfully + */ + public function closeSession() + { + return true; + } + + /** + * Session read handler. + * This method should be overridden if [[useCustomStorage()]] returns true. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + return ''; + } + + /** + * Session write handler. + * This method should be overridden if [[useCustomStorage()]] returns true. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + return true; + } + + /** + * Session destroy handler. + * This method should be overridden if [[useCustomStorage()]] returns true. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + return true; + } + + /** + * Session GC (garbage collection) handler. + * This method should be overridden if [[useCustomStorage()]] returns true. + * Do not call this method directly. + * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. + * @return boolean whether session is GCed successfully + */ + public function gcSession($maxLifetime) + { + return true; + } + + /** + * Returns an iterator for traversing the session variables. + * This method is required by the interface IteratorAggregate. + * @return SessionIterator an iterator for traversing the session variables. + */ + public function getIterator() + { + return new SessionIterator; + } + + /** + * Returns the number of items in the session. + * @return integer the number of session variables + */ + public function getCount() + { + return count($_SESSION); + } + + /** + * Returns the number of items in the session. + * This method is required by Countable interface. + * @return integer number of items in the session. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the session variable value with the session variable name. + * If the session variable does not exist, the `$defaultValue` will be returned. + * @param string $key the session variable name + * @param mixed $defaultValue the default value to be returned when the session variable does not exist. + * @return mixed the session variable value, or $defaultValue if the session variable does not exist. + */ + public function get($key, $defaultValue = null) + { + $this->open(); + + return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; + } + + /** + * Adds a session variable. + * If the specified name already exists, the old value will be overwritten. + * @param string $key session variable name + * @param mixed $value session variable value + */ + public function set($key, $value) + { + $this->open(); + $_SESSION[$key] = $value; + } + + /** + * Removes a session variable. + * @param string $key the name of the session variable to be removed + * @return mixed the removed value, null if no such session variable. + */ + public function remove($key) + { + $this->open(); + if (isset($_SESSION[$key])) { + $value = $_SESSION[$key]; + unset($_SESSION[$key]); + + return $value; + } else { + return null; + } + } + + /** + * Removes all session variables + */ + public function removeAll() + { + $this->open(); + foreach (array_keys($_SESSION) as $key) { + unset($_SESSION[$key]); + } + } + + /** + * @param mixed $key session variable name + * @return boolean whether there is the named session variable + */ + public function has($key) + { + $this->open(); + + return isset($_SESSION[$key]); + } + + /** + * Updates the counters for flash messages and removes outdated flash messages. + * This method should only be called once in [[init()]]. + */ + protected function updateFlashCounters() + { + $counters = $this->get($this->flashParam, []); + if (is_array($counters)) { + foreach ($counters as $key => $count) { + if ($count) { + unset($counters[$key], $_SESSION[$key]); + } else { + $counters[$key]++; + } + } + $_SESSION[$this->flashParam] = $counters; + } else { + // fix the unexpected problem that flashParam doesn't return an array + unset($_SESSION[$this->flashParam]); + } + } + + /** + * Returns a flash message. + * A flash message is available only in the current request and the next request. + * @param string $key the key identifying the flash message + * @param mixed $defaultValue value to be returned if the flash message does not exist. + * @param boolean $delete whether to delete this flash message right after this method is called. + * If false, the flash message will be automatically deleted after the next request. + * @return mixed the flash message + */ + public function getFlash($key, $defaultValue = null, $delete = false) + { + $counters = $this->get($this->flashParam, []); + if (isset($counters[$key])) { + $value = $this->get($key, $defaultValue); + if ($delete) { + $this->removeFlash($key); + } + + return $value; + } else { + return $defaultValue; + } + } + + /** + * Returns all flash messages. + * @return array flash messages (key => message). + */ + public function getAllFlashes() + { + $counters = $this->get($this->flashParam, []); + $flashes = []; + foreach (array_keys($counters) as $key) { + if (isset($_SESSION[$key])) { + $flashes[$key] = $_SESSION[$key]; + } + } + + return $flashes; + } + + /** + * Stores a flash message. + * A flash message is available only in the current request and the next request. + * @param string $key the key identifying the flash message. Note that flash messages + * and normal session variables share the same name space. If you have a normal + * session variable using the same name, its value will be overwritten by this method. + * @param mixed $value flash message + */ + public function setFlash($key, $value = true) + { + $counters = $this->get($this->flashParam, []); + $counters[$key] = 0; + $_SESSION[$key] = $value; + $_SESSION[$this->flashParam] = $counters; + } + + /** + * Removes a flash message. + * Note that flash messages will be automatically removed after the next request. + * @param string $key the key identifying the flash message. Note that flash messages + * and normal session variables share the same name space. If you have a normal + * session variable using the same name, it will be removed by this method. + * @return mixed the removed flash message. Null if the flash message does not exist. + */ + public function removeFlash($key) + { + $counters = $this->get($this->flashParam, []); + $value = isset($_SESSION[$key], $counters[$key]) ? $_SESSION[$key] : null; + unset($counters[$key], $_SESSION[$key]); + $_SESSION[$this->flashParam] = $counters; + + return $value; + } + + /** + * Removes all flash messages. + * Note that flash messages and normal session variables share the same name space. + * If you have a normal session variable using the same name, it will be removed + * by this method. + */ + public function removeAllFlashes() + { + $counters = $this->get($this->flashParam, []); + foreach (array_keys($counters) as $key) { + unset($_SESSION[$key]); + } + unset($_SESSION[$this->flashParam]); + } + + /** + * Returns a value indicating whether there is a flash message associated with the specified key. + * @param string $key key identifying the flash message + * @return boolean whether the specified flash message exists + */ + public function hasFlash($key) + { + return $this->getFlash($key) !== null; + } + + /** + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean + */ + public function offsetExists($offset) + { + $this->open(); + + return isset($_SESSION[$offset]); + } + + /** + * This method is required by the interface ArrayAccess. + * @param integer $offset the offset to retrieve element. + * @return mixed the element at the offset, null if no element is found at the offset + */ + public function offsetGet($offset) + { + $this->open(); + + return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; + } + + /** + * This method is required by the interface ArrayAccess. + * @param integer $offset the offset to set element + * @param mixed $item the element value + */ + public function offsetSet($offset, $item) + { + $this->open(); + $_SESSION[$offset] = $item; + } + + /** + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to unset element + */ + public function offsetUnset($offset) + { + $this->open(); + unset($_SESSION[$offset]); + } } diff --git a/framework/web/SessionIterator.php b/framework/web/SessionIterator.php index c960dd45e48..7e4c9c0a577 100644 --- a/framework/web/SessionIterator.php +++ b/framework/web/SessionIterator.php @@ -15,70 +15,70 @@ */ class SessionIterator implements \Iterator { - /** - * @var array list of keys in the map - */ - private $_keys; - /** - * @var mixed current key - */ - private $_key; + /** + * @var array list of keys in the map + */ + private $_keys; + /** + * @var mixed current key + */ + private $_key; - /** - * Constructor. - */ - public function __construct() - { - $this->_keys = array_keys($_SESSION); - } + /** + * Constructor. + */ + public function __construct() + { + $this->_keys = array_keys($_SESSION); + } - /** - * Rewinds internal array pointer. - * This method is required by the interface Iterator. - */ - public function rewind() - { - $this->_key = reset($this->_keys); - } + /** + * Rewinds internal array pointer. + * This method is required by the interface Iterator. + */ + public function rewind() + { + $this->_key = reset($this->_keys); + } - /** - * Returns the key of the current array element. - * This method is required by the interface Iterator. - * @return mixed the key of the current array element - */ - public function key() - { - return $this->_key; - } + /** + * Returns the key of the current array element. + * This method is required by the interface Iterator. + * @return mixed the key of the current array element + */ + public function key() + { + return $this->_key; + } - /** - * Returns the current array element. - * This method is required by the interface Iterator. - * @return mixed the current array element - */ - public function current() - { - return isset($_SESSION[$this->_key]) ? $_SESSION[$this->_key] : null; - } + /** + * Returns the current array element. + * This method is required by the interface Iterator. + * @return mixed the current array element + */ + public function current() + { + return isset($_SESSION[$this->_key]) ? $_SESSION[$this->_key] : null; + } - /** - * Moves the internal pointer to the next array element. - * This method is required by the interface Iterator. - */ - public function next() - { - do { - $this->_key = next($this->_keys); - } while (!isset($_SESSION[$this->_key]) && $this->_key !== false); - } + /** + * Moves the internal pointer to the next array element. + * This method is required by the interface Iterator. + */ + public function next() + { + do { + $this->_key = next($this->_keys); + } while (!isset($_SESSION[$this->_key]) && $this->_key !== false); + } - /** - * Returns whether there is an element at current position. - * This method is required by the interface Iterator. - * @return boolean - */ - public function valid() - { - return $this->_key !== false; - } + /** + * Returns whether there is an element at current position. + * This method is required by the interface Iterator. + * @return boolean + */ + public function valid() + { + return $this->_key !== false; + } } diff --git a/framework/web/TooManyRequestsHttpException.php b/framework/web/TooManyRequestsHttpException.php index b5eb8898a5e..ebaa6c656b7 100644 --- a/framework/web/TooManyRequestsHttpException.php +++ b/framework/web/TooManyRequestsHttpException.php @@ -20,14 +20,14 @@ */ class TooManyRequestsHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(429, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(429, $message, $code, $previous); + } } diff --git a/framework/web/UnauthorizedHttpException.php b/framework/web/UnauthorizedHttpException.php index 0bea2090465..b8f84192aa2 100644 --- a/framework/web/UnauthorizedHttpException.php +++ b/framework/web/UnauthorizedHttpException.php @@ -21,14 +21,14 @@ */ class UnauthorizedHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(401, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(401, $message, $code, $previous); + } } diff --git a/framework/web/UnsupportedMediaTypeHttpException.php b/framework/web/UnsupportedMediaTypeHttpException.php index 715117e0ed3..d79ed4a5d82 100644 --- a/framework/web/UnsupportedMediaTypeHttpException.php +++ b/framework/web/UnsupportedMediaTypeHttpException.php @@ -21,14 +21,14 @@ */ class UnsupportedMediaTypeHttpException extends HttpException { - /** - * Constructor. - * @param string $message error message - * @param integer $code error code - * @param \Exception $previous The previous exception used for the exception chaining. - */ - public function __construct($message = null, $code = 0, \Exception $previous = null) - { - parent::__construct(415, $message, $code, $previous); - } + /** + * Constructor. + * @param string $message error message + * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + parent::__construct(415, $message, $code, $previous); + } } diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php index dd2bb6157ad..0f9eaaec27f 100644 --- a/framework/web/UploadedFile.php +++ b/framework/web/UploadedFile.php @@ -28,209 +28,214 @@ */ class UploadedFile extends Object { - private static $_files; - - /** - * @var string the original name of the file being uploaded - */ - public $name; - /** - * @var string the path of the uploaded file on the server. - * Note, this is a temporary file which will be automatically deleted by PHP - * after the current request is processed. - */ - public $tempName; - /** - * @var string the MIME-type of the uploaded file (such as "image/gif"). - * Since this MIME type is not checked on the server side, do not take this value for granted. - * Instead, use [[FileHelper::getMimeType()]] to determine the exact MIME type. - */ - public $type; - /** - * @var integer the actual size of the uploaded file in bytes - */ - public $size; - /** - * @var integer an error code describing the status of this file uploading. - * @see http://www.php.net/manual/en/features.file-upload.errors.php - */ - public $error; - - - /** - * String output. - * This is PHP magic method that returns string representation of an object. - * The implementation here returns the uploaded file's name. - * @return string the string representation of the object - */ - public function __toString() - { - return $this->name; - } - - /** - * Returns an uploaded file for the given model attribute. - * The file should be uploaded using [[ActiveForm::fileInput()]]. - * @param \yii\base\Model $model the data model - * @param string $attribute the attribute name. The attribute name may contain array indexes. - * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array. - * @return UploadedFile the instance of the uploaded file. - * Null is returned if no file is uploaded for the specified model attribute. - * @see getInstanceByName() - */ - public static function getInstance($model, $attribute) - { - $name = Html::getInputName($model, $attribute); - return static::getInstanceByName($name); - } - - /** - * Returns all uploaded files for the given model attribute. - * @param \yii\base\Model $model the data model - * @param string $attribute the attribute name. The attribute name may contain array indexes - * for tabular file uploading, e.g. '[1]file'. - * @return UploadedFile[] array of UploadedFile objects. - * Empty array is returned if no available file was found for the given attribute. - */ - public static function getInstances($model, $attribute) - { - $name = Html::getInputName($model, $attribute); - return static::getInstancesByName($name); - } - - /** - * Returns an uploaded file according to the given file input name. - * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). - * @param string $name the name of the file input field. - * @return UploadedFile the instance of the uploaded file. - * Null is returned if no file is uploaded for the specified name. - */ - public static function getInstanceByName($name) - { - $files = static::loadFiles(); - return isset($files[$name]) ? $files[$name] : null; - } - - /** - * Returns an array of uploaded files corresponding to the specified file input name. - * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]', - * 'files[n]'..., and you can retrieve them all by passing 'files' as the name. - * @param string $name the name of the array of files - * @return UploadedFile[] the array of CUploadedFile objects. Empty array is returned - * if no adequate upload was found. Please note that this array will contain - * all files from all sub-arrays regardless how deeply nested they are. - */ - public static function getInstancesByName($name) - { - $files = static::loadFiles(); - if (isset($files[$name])) { - return [$files[$name]]; - } - $results = []; - foreach ($files as $key => $file) { - if (strpos($key, "{$name}[") === 0) { - $results[] = self::$_files[$key]; - } - } - return $results; - } - - /** - * Cleans up the loaded UploadedFile instances. - * This method is mainly used by test scripts to set up a fixture. - */ - public static function reset() - { - self::$_files = null; - } - - /** - * Saves the uploaded file. - * Note that this method uses php's move_uploaded_file() method. If the target file `$file` - * already exists, it will be overwritten. - * @param string $file the file path used to save the uploaded file - * @param boolean $deleteTempFile whether to delete the temporary file after saving. - * If true, you will not be able to save the uploaded file again in the current request. - * @return boolean true whether the file is saved successfully - * @see error - */ - public function saveAs($file, $deleteTempFile = true) - { - if ($this->error == UPLOAD_ERR_OK) { - if ($deleteTempFile) { - return move_uploaded_file($this->tempName, $file); - } elseif (is_uploaded_file($this->tempName)) { - return copy($this->tempName, $file); - } - } - return false; - } - - /** - * @return string original file base name - */ - public function getBaseName() - { - return pathinfo($this->name, PATHINFO_FILENAME); - } - - /** - * @return string file extension - */ - public function getExtension() - { - return strtolower(pathinfo($this->name, PATHINFO_EXTENSION)); - } - - /** - * @return boolean whether there is an error with the uploaded file. - * Check [[error]] for detailed error code information. - */ - public function getHasError() - { - return $this->error != UPLOAD_ERR_OK; - } - - /** - * Creates UploadedFile instances from $_FILE. - * @return array the UploadedFile instances - */ - private static function loadFiles() - { - if (self::$_files === null) { - self::$_files = []; - if (isset($_FILES) && is_array($_FILES)) { - foreach ($_FILES as $class => $info) { - self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); - } - } - } - return self::$_files; - } - - /** - * Creates UploadedFile instances from $_FILE recursively. - * @param string $key key for identifying uploaded file: class name and sub-array indexes - * @param mixed $names file names provided by PHP - * @param mixed $tempNames temporary file names provided by PHP - * @param mixed $types file types provided by PHP - * @param mixed $sizes file sizes provided by PHP - * @param mixed $errors uploading issues provided by PHP - */ - private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) - { - if (is_array($names)) { - foreach ($names as $i => $name) { - self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); - } - } else { - self::$_files[$key] = new static([ - 'name' => $names, - 'tempName' => $tempNames, - 'type' => $types, - 'size' => $sizes, - 'error' => $errors, - ]); - } - } + private static $_files; + + /** + * @var string the original name of the file being uploaded + */ + public $name; + /** + * @var string the path of the uploaded file on the server. + * Note, this is a temporary file which will be automatically deleted by PHP + * after the current request is processed. + */ + public $tempName; + /** + * @var string the MIME-type of the uploaded file (such as "image/gif"). + * Since this MIME type is not checked on the server side, do not take this value for granted. + * Instead, use [[FileHelper::getMimeType()]] to determine the exact MIME type. + */ + public $type; + /** + * @var integer the actual size of the uploaded file in bytes + */ + public $size; + /** + * @var integer an error code describing the status of this file uploading. + * @see http://www.php.net/manual/en/features.file-upload.errors.php + */ + public $error; + + /** + * String output. + * This is PHP magic method that returns string representation of an object. + * The implementation here returns the uploaded file's name. + * @return string the string representation of the object + */ + public function __toString() + { + return $this->name; + } + + /** + * Returns an uploaded file for the given model attribute. + * The file should be uploaded using [[ActiveForm::fileInput()]]. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes. + * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified model attribute. + * @see getInstanceByName() + */ + public static function getInstance($model, $attribute) + { + $name = Html::getInputName($model, $attribute); + + return static::getInstanceByName($name); + } + + /** + * Returns all uploaded files for the given model attribute. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes + * for tabular file uploading, e.g. '[1]file'. + * @return UploadedFile[] array of UploadedFile objects. + * Empty array is returned if no available file was found for the given attribute. + */ + public static function getInstances($model, $attribute) + { + $name = Html::getInputName($model, $attribute); + + return static::getInstancesByName($name); + } + + /** + * Returns an uploaded file according to the given file input name. + * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). + * @param string $name the name of the file input field. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified name. + */ + public static function getInstanceByName($name) + { + $files = static::loadFiles(); + + return isset($files[$name]) ? $files[$name] : null; + } + + /** + * Returns an array of uploaded files corresponding to the specified file input name. + * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]', + * 'files[n]'..., and you can retrieve them all by passing 'files' as the name. + * @param string $name the name of the array of files + * @return UploadedFile[] the array of CUploadedFile objects. Empty array is returned + * if no adequate upload was found. Please note that this array will contain + * all files from all sub-arrays regardless how deeply nested they are. + */ + public static function getInstancesByName($name) + { + $files = static::loadFiles(); + if (isset($files[$name])) { + return [$files[$name]]; + } + $results = []; + foreach ($files as $key => $file) { + if (strpos($key, "{$name}[") === 0) { + $results[] = self::$_files[$key]; + } + } + + return $results; + } + + /** + * Cleans up the loaded UploadedFile instances. + * This method is mainly used by test scripts to set up a fixture. + */ + public static function reset() + { + self::$_files = null; + } + + /** + * Saves the uploaded file. + * Note that this method uses php's move_uploaded_file() method. If the target file `$file` + * already exists, it will be overwritten. + * @param string $file the file path used to save the uploaded file + * @param boolean $deleteTempFile whether to delete the temporary file after saving. + * If true, you will not be able to save the uploaded file again in the current request. + * @return boolean true whether the file is saved successfully + * @see error + */ + public function saveAs($file, $deleteTempFile = true) + { + if ($this->error == UPLOAD_ERR_OK) { + if ($deleteTempFile) { + return move_uploaded_file($this->tempName, $file); + } elseif (is_uploaded_file($this->tempName)) { + return copy($this->tempName, $file); + } + } + + return false; + } + + /** + * @return string original file base name + */ + public function getBaseName() + { + return pathinfo($this->name, PATHINFO_FILENAME); + } + + /** + * @return string file extension + */ + public function getExtension() + { + return strtolower(pathinfo($this->name, PATHINFO_EXTENSION)); + } + + /** + * @return boolean whether there is an error with the uploaded file. + * Check [[error]] for detailed error code information. + */ + public function getHasError() + { + return $this->error != UPLOAD_ERR_OK; + } + + /** + * Creates UploadedFile instances from $_FILE. + * @return array the UploadedFile instances + */ + private static function loadFiles() + { + if (self::$_files === null) { + self::$_files = []; + if (isset($_FILES) && is_array($_FILES)) { + foreach ($_FILES as $class => $info) { + self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); + } + } + } + + return self::$_files; + } + + /** + * Creates UploadedFile instances from $_FILE recursively. + * @param string $key key for identifying uploaded file: class name and sub-array indexes + * @param mixed $names file names provided by PHP + * @param mixed $tempNames temporary file names provided by PHP + * @param mixed $types file types provided by PHP + * @param mixed $sizes file sizes provided by PHP + * @param mixed $errors uploading issues provided by PHP + */ + private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) + { + if (is_array($names)) { + foreach ($names as $i => $name) { + self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); + } + } else { + self::$_files[$key] = new static([ + 'name' => $names, + 'tempName' => $tempNames, + 'type' => $types, + 'size' => $sizes, + 'error' => $errors, + ]); + } + } } diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 0341d4aeb28..c335112e0f4 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -40,309 +40,316 @@ */ class UrlManager extends Component { - /** - * @var boolean whether to enable pretty URLs. Instead of putting all parameters in the query - * string part of a URL, pretty URLs allow using path info to represent some of the parameters - * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of - * "/index.php?r=news/view&id=100". - */ - public $enablePrettyUrl = false; - /** - * @var boolean whether to enable strict parsing. If strict parsing is enabled, the incoming - * requested URL must match at least one of the [[rules]] in order to be treated as a valid request. - * Otherwise, the path info part of the request will be treated as the requested route. - * This property is used only when [[enablePrettyUrl]] is true. - */ - public $enableStrictParsing = false; - /** - * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is true. - * This property is used only if [[enablePrettyUrl]] is true. Each element in the array - * is the configuration array for creating a single URL rule. The configuration will - * be merged with [[ruleConfig]] first before it is used for creating the rule object. - * - * A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]] - * and [[UrlRule::route|route]]: `'pattern' => 'route'`. That is, instead of using a configuration - * array, one can use the key to represent the pattern and the value the corresponding route. - * For example, `'post/' => 'post/view'`. - * - * For RESTful routing the mentioned shortcut format also allows you to specify the - * [[UrlRule::verb|HTTP verb]] that the rule should apply for. - * You can do that by prepending it to the pattern, separated by space. - * For example, `'PUT post/' => 'post/update'`. - * You may specify multiple verbs by separating them with comma - * like this: `'POST,PUT post/index' => 'post/create'`. - * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE. - * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way - * so you normally would not specify a verb for normal GET request. - * - * Here is an example configuration for RESTful CRUD controller: - * - * ~~~php - * [ - * 'dashboard' => 'site/index', - * - * 'POST s' => '/create', - * 's' => '/index', - * - * 'PUT /' => '/update', - * 'DELETE /' => '/delete', - * '/' => '/view', - * ]; - * ~~~ - * - * Note that if you modify this property after the UrlManager object is created, make sure - * you populate the array with rule objects instead of rule configurations. - */ - public $rules = []; - /** - * @var string the URL suffix used when in 'path' format. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * This property is used only if [[enablePrettyUrl]] is true. - */ - public $suffix; - /** - * @var boolean whether to show entry script name in the constructed URL. Defaults to true. - * This property is used only if [[enablePrettyUrl]] is true. - */ - public $showScriptName = true; - /** - * @var string the GET parameter name for route. This property is used only if [[enablePrettyUrl]] is false. - */ - public $routeParam = 'r'; - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * Compiled URL rules will be cached through this cache object, if it is available. - * - * After the UrlManager object is created, if you want to change this property, - * you should only assign it with a cache object. - * Set this property to null if you do not want to cache the URL rules. - */ - public $cache = 'cache'; - /** - * @var array the default configuration of URL rules. Individual rule configurations - * specified via [[rules]] will take precedence when the same property of the rule is configured. - */ - public $ruleConfig = ['class' => 'yii\web\UrlRule']; + /** + * @var boolean whether to enable pretty URLs. Instead of putting all parameters in the query + * string part of a URL, pretty URLs allow using path info to represent some of the parameters + * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of + * "/index.php?r=news/view&id=100". + */ + public $enablePrettyUrl = false; + /** + * @var boolean whether to enable strict parsing. If strict parsing is enabled, the incoming + * requested URL must match at least one of the [[rules]] in order to be treated as a valid request. + * Otherwise, the path info part of the request will be treated as the requested route. + * This property is used only when [[enablePrettyUrl]] is true. + */ + public $enableStrictParsing = false; + /** + * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is true. + * This property is used only if [[enablePrettyUrl]] is true. Each element in the array + * is the configuration array for creating a single URL rule. The configuration will + * be merged with [[ruleConfig]] first before it is used for creating the rule object. + * + * A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]] + * and [[UrlRule::route|route]]: `'pattern' => 'route'`. That is, instead of using a configuration + * array, one can use the key to represent the pattern and the value the corresponding route. + * For example, `'post/' => 'post/view'`. + * + * For RESTful routing the mentioned shortcut format also allows you to specify the + * [[UrlRule::verb|HTTP verb]] that the rule should apply for. + * You can do that by prepending it to the pattern, separated by space. + * For example, `'PUT post/' => 'post/update'`. + * You may specify multiple verbs by separating them with comma + * like this: `'POST,PUT post/index' => 'post/create'`. + * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE. + * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way + * so you normally would not specify a verb for normal GET request. + * + * Here is an example configuration for RESTful CRUD controller: + * + * ~~~php + * [ + * 'dashboard' => 'site/index', + * + * 'POST s' => '/create', + * 's' => '/index', + * + * 'PUT /' => '/update', + * 'DELETE /' => '/delete', + * '/' => '/view', + * ]; + * ~~~ + * + * Note that if you modify this property after the UrlManager object is created, make sure + * you populate the array with rule objects instead of rule configurations. + */ + public $rules = []; + /** + * @var string the URL suffix used when in 'path' format. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * This property is used only if [[enablePrettyUrl]] is true. + */ + public $suffix; + /** + * @var boolean whether to show entry script name in the constructed URL. Defaults to true. + * This property is used only if [[enablePrettyUrl]] is true. + */ + public $showScriptName = true; + /** + * @var string the GET parameter name for route. This property is used only if [[enablePrettyUrl]] is false. + */ + public $routeParam = 'r'; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * Compiled URL rules will be cached through this cache object, if it is available. + * + * After the UrlManager object is created, if you want to change this property, + * you should only assign it with a cache object. + * Set this property to null if you do not want to cache the URL rules. + */ + public $cache = 'cache'; + /** + * @var array the default configuration of URL rules. Individual rule configurations + * specified via [[rules]] will take precedence when the same property of the rule is configured. + */ + public $ruleConfig = ['class' => 'yii\web\UrlRule']; - private $_baseUrl; - private $_hostInfo; + private $_baseUrl; + private $_hostInfo; - /** - * Initializes UrlManager. - */ - public function init() - { - parent::init(); - $this->compileRules(); - } + /** + * Initializes UrlManager. + */ + public function init() + { + parent::init(); + $this->compileRules(); + } - /** - * Parses the URL rules. - */ - protected function compileRules() - { - if (!$this->enablePrettyUrl || empty($this->rules)) { - return; - } - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } - if ($this->cache instanceof Cache) { - $key = __CLASS__; - $hash = md5(json_encode($this->rules)); - if (($data = $this->cache->get($key)) !== false && isset($data[1]) && $data[1] === $hash) { - $this->rules = $data[0]; - return; - } - } + /** + * Parses the URL rules. + */ + protected function compileRules() + { + if (!$this->enablePrettyUrl || empty($this->rules)) { + return; + } + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if ($this->cache instanceof Cache) { + $key = __CLASS__; + $hash = md5(json_encode($this->rules)); + if (($data = $this->cache->get($key)) !== false && isset($data[1]) && $data[1] === $hash) { + $this->rules = $data[0]; - $rules = []; - $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS'; - foreach ($this->rules as $key => $rule) { - if (!is_array($rule)) { - $rule = ['route' => $rule]; - if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) { - $rule['verb'] = explode(',', $matches[1]); - $rule['mode'] = UrlRule::PARSING_ONLY; - $key = $matches[4]; - } - $rule['pattern'] = $key; - } - $rule = Yii::createObject(array_merge($this->ruleConfig, $rule)); - if (!$rule instanceof UrlRuleInterface) { - throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.'); - } - $rules[] = $rule; - } - $this->rules = $rules; + return; + } + } - if (isset($key, $hash)) { - $this->cache->set($key, [$this->rules, $hash]); - } - } + $rules = []; + $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS'; + foreach ($this->rules as $key => $rule) { + if (!is_array($rule)) { + $rule = ['route' => $rule]; + if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) { + $rule['verb'] = explode(',', $matches[1]); + $rule['mode'] = UrlRule::PARSING_ONLY; + $key = $matches[4]; + } + $rule['pattern'] = $key; + } + $rule = Yii::createObject(array_merge($this->ruleConfig, $rule)); + if (!$rule instanceof UrlRuleInterface) { + throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.'); + } + $rules[] = $rule; + } + $this->rules = $rules; - /** - * Parses the user request. - * @param Request $request the request component - * @return array|boolean the route and the associated parameters. The latter is always empty - * if [[enablePrettyUrl]] is false. False is returned if the current request cannot be successfully parsed. - */ - public function parseRequest($request) - { - if ($this->enablePrettyUrl) { - $pathInfo = $request->getPathInfo(); - /** @var UrlRule $rule */ - foreach ($this->rules as $rule) { - if (($result = $rule->parseRequest($this, $request)) !== false) { - return $result; - } - } + if (isset($key, $hash)) { + $this->cache->set($key, [$this->rules, $hash]); + } + } - if ($this->enableStrictParsing) { - return false; - } + /** + * Parses the user request. + * @param Request $request the request component + * @return array|boolean the route and the associated parameters. The latter is always empty + * if [[enablePrettyUrl]] is false. False is returned if the current request cannot be successfully parsed. + */ + public function parseRequest($request) + { + if ($this->enablePrettyUrl) { + $pathInfo = $request->getPathInfo(); + /** @var UrlRule $rule */ + foreach ($this->rules as $rule) { + if (($result = $rule->parseRequest($this, $request)) !== false) { + return $result; + } + } - Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__); + if ($this->enableStrictParsing) { + return false; + } - $suffix = (string)$this->suffix; - if ($suffix !== '' && $pathInfo !== '') { - $n = strlen($this->suffix); - if (substr($pathInfo, -$n) === $this->suffix) { - $pathInfo = substr($pathInfo, 0, -$n); - if ($pathInfo === '') { - // suffix alone is not allowed - return false; - } - } else { - // suffix doesn't match - return false; - } - } + Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__); - return [$pathInfo, []]; - } else { - Yii::trace('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__); - $route = $request->getQueryParam($this->routeParam, ''); - if (is_array($route)) { - $route = ''; - } - return [(string)$route, []]; - } - } + $suffix = (string) $this->suffix; + if ($suffix !== '' && $pathInfo !== '') { + $n = strlen($this->suffix); + if (substr($pathInfo, -$n) === $this->suffix) { + $pathInfo = substr($pathInfo, 0, -$n); + if ($pathInfo === '') { + // suffix alone is not allowed + return false; + } + } else { + // suffix doesn't match + return false; + } + } - /** - * Creates a URL using the given route and parameters. - * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL. - * @param string|array $params route as a string or route and parameters in form of ['route', 'param1' => 'value1', 'param2' => 'value2'] - * @return string the created URL - */ - public function createUrl($params) - { - $params = (array)$params; - $anchor = isset($params['#']) ? '#' . $params['#'] : ''; - unset($params['#'], $params[$this->routeParam]); + return [$pathInfo, []]; + } else { + Yii::trace('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__); + $route = $request->getQueryParam($this->routeParam, ''); + if (is_array($route)) { + $route = ''; + } - $route = trim($params[0], '/'); - unset($params[0]); - $baseUrl = $this->getBaseUrl(); + return [(string) $route, []]; + } + } - if ($this->enablePrettyUrl) { - /** @var UrlRule $rule */ - foreach ($this->rules as $rule) { - if (($url = $rule->createUrl($this, $route, $params)) !== false) { - if (strpos($url, '://') !== false) { - if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) { - return substr($url, 0, $pos) . $baseUrl . substr($url, $pos); - } else { - return $url . $baseUrl . $anchor; - } - } else { - return "$baseUrl/{$url}{$anchor}"; - } - } - } + /** + * Creates a URL using the given route and parameters. + * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL. + * @param string|array $params route as a string or route and parameters in form of ['route', 'param1' => 'value1', 'param2' => 'value2'] + * @return string the created URL + */ + public function createUrl($params) + { + $params = (array) $params; + $anchor = isset($params['#']) ? '#' . $params['#'] : ''; + unset($params['#'], $params[$this->routeParam]); - if ($this->suffix !== null) { - $route .= $this->suffix; - } - if (!empty($params) && ($query = http_build_query($params)) !== '') { - $route .= '?' . $query; - } - return "$baseUrl/{$route}{$anchor}"; - } else { - $url = "$baseUrl?{$this->routeParam}=$route"; - if (!empty($params) && ($query = http_build_query($params)) !== '') { - $url .= '&' . $query; - } - return $url . $anchor; - } - } + $route = trim($params[0], '/'); + unset($params[0]); + $baseUrl = $this->getBaseUrl(); - /** - * Creates an absolute URL using the given route and parameters. - * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]]. - * @param string|array $params route as a string or route and parameters in form of ['route', 'param1' => 'value1', 'param2' => 'value2'] - * @param string $schema the schema to use for the url. e.g. 'http' or 'https'. If not specified - * the schema of the current request will be used. - * @return string the created URL - * @see createUrl() - */ - public function createAbsoluteUrl($params, $schema = null) - { - $params = (array)$params; - $url = $this->createUrl($params); - if (strpos($url, '://') === false) { - $url = $this->getHostInfo() . $url; - } - if ($schema && ($pos = strpos($url, '://')) !== false) { - $url = $schema . substr($url, $pos); - } - return $url; - } + if ($this->enablePrettyUrl) { + /** @var UrlRule $rule */ + foreach ($this->rules as $rule) { + if (($url = $rule->createUrl($this, $route, $params)) !== false) { + if (strpos($url, '://') !== false) { + if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) { + return substr($url, 0, $pos) . $baseUrl . substr($url, $pos); + } else { + return $url . $baseUrl . $anchor; + } + } else { + return "$baseUrl/{$url}{$anchor}"; + } + } + } - /** - * Returns the base URL that is used by [[createUrl()]] to prepend URLs it creates. - * It defaults to [[Request::scriptUrl]] if [[showScriptName]] is true or [[enablePrettyUrl]] is false; - * otherwise, it defaults to [[Request::baseUrl]]. - * @return string the base URL that is used by [[createUrl()]] to prepend URLs it creates. - */ - public function getBaseUrl() - { - if ($this->_baseUrl === null) { - /** @var \yii\web\Request $request */ - $request = Yii::$app->getRequest(); - $this->_baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $request->getScriptUrl() : $request->getBaseUrl(); - } - return $this->_baseUrl; - } + if ($this->suffix !== null) { + $route .= $this->suffix; + } + if (!empty($params) && ($query = http_build_query($params)) !== '') { + $route .= '?' . $query; + } - /** - * Sets the base URL that is used by [[createUrl()]] to prepend URLs it creates. - * @param string $value the base URL that is used by [[createUrl()]] to prepend URLs it creates. - */ - public function setBaseUrl($value) - { - $this->_baseUrl = rtrim($value, '/'); - } + return "$baseUrl/{$route}{$anchor}"; + } else { + $url = "$baseUrl?{$this->routeParam}=$route"; + if (!empty($params) && ($query = http_build_query($params)) !== '') { + $url .= '&' . $query; + } - /** - * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. - * @return string the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. - */ - public function getHostInfo() - { - if ($this->_hostInfo === null) { - $this->_hostInfo = Yii::$app->getRequest()->getHostInfo(); - } - return $this->_hostInfo; - } + return $url . $anchor; + } + } - /** - * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. - * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. - */ - public function setHostInfo($value) - { - $this->_hostInfo = rtrim($value, '/'); - } + /** + * Creates an absolute URL using the given route and parameters. + * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]]. + * @param string|array $params route as a string or route and parameters in form of ['route', 'param1' => 'value1', 'param2' => 'value2'] + * @param string $schema the schema to use for the url. e.g. 'http' or 'https'. If not specified + * the schema of the current request will be used. + * @return string the created URL + * @see createUrl() + */ + public function createAbsoluteUrl($params, $schema = null) + { + $params = (array) $params; + $url = $this->createUrl($params); + if (strpos($url, '://') === false) { + $url = $this->getHostInfo() . $url; + } + if ($schema && ($pos = strpos($url, '://')) !== false) { + $url = $schema . substr($url, $pos); + } + + return $url; + } + + /** + * Returns the base URL that is used by [[createUrl()]] to prepend URLs it creates. + * It defaults to [[Request::scriptUrl]] if [[showScriptName]] is true or [[enablePrettyUrl]] is false; + * otherwise, it defaults to [[Request::baseUrl]]. + * @return string the base URL that is used by [[createUrl()]] to prepend URLs it creates. + */ + public function getBaseUrl() + { + if ($this->_baseUrl === null) { + /** @var \yii\web\Request $request */ + $request = Yii::$app->getRequest(); + $this->_baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $request->getScriptUrl() : $request->getBaseUrl(); + } + + return $this->_baseUrl; + } + + /** + * Sets the base URL that is used by [[createUrl()]] to prepend URLs it creates. + * @param string $value the base URL that is used by [[createUrl()]] to prepend URLs it creates. + */ + public function setBaseUrl($value) + { + $this->_baseUrl = rtrim($value, '/'); + } + + /** + * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + * @return string the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + */ + public function getHostInfo() + { + if ($this->_hostInfo === null) { + $this->_hostInfo = Yii::$app->getRequest()->getHostInfo(); + } + + return $this->_hostInfo; + } + + /** + * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend URLs it creates. + */ + public function setHostInfo($value) + { + $this->_hostInfo = rtrim($value, '/'); + } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 7b671f5f4d5..d925fc3ba49 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -29,309 +29,311 @@ */ class UrlRule extends Object implements UrlRuleInterface { - /** - * Set [[mode]] with this value to mark that this rule is for URL parsing only - */ - const PARSING_ONLY = 1; - /** - * Set [[mode]] with this value to mark that this rule is for URL creation only - */ - const CREATION_ONLY = 2; + /** + * Set [[mode]] with this value to mark that this rule is for URL parsing only + */ + const PARSING_ONLY = 1; + /** + * Set [[mode]] with this value to mark that this rule is for URL creation only + */ + const CREATION_ONLY = 2; - /** - * @var string the name of this rule. If not set, it will use [[pattern]] as the name. - */ - public $name; - /** - * @var string the pattern used to parse and create the path info part of a URL. - * @see host - */ - public $pattern; - /** - * @var string the pattern used to parse and create the host info part of a URL (e.g. `http://example.com`). - * @see pattern - */ - public $host; - /** - * @var string the route to the controller action - */ - public $route; - /** - * @var array the default GET parameters (name => value) that this rule provides. - * When this rule is used to parse the incoming request, the values declared in this property - * will be injected into $_GET. - */ - public $defaults = []; - /** - * @var string the URL suffix used for this rule. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * If not, the value of [[UrlManager::suffix]] will be used. - */ - public $suffix; - /** - * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. - * Use array to represent multiple verbs that this rule may match. - * If this property is not set, the rule can match any verb. - * Note that this property is only used when parsing a request. It is ignored for URL creation. - */ - public $verb; - /** - * @var integer a value indicating if this rule should be used for both request parsing and URL creation, - * parsing only, or creation only. - * If not set or 0, it means the rule is both request parsing and URL creation. - * If it is [[PARSING_ONLY]], the rule is for request parsing only. - * If it is [[CREATION_ONLY]], the rule is for URL creation only. - */ - public $mode; + /** + * @var string the name of this rule. If not set, it will use [[pattern]] as the name. + */ + public $name; + /** + * @var string the pattern used to parse and create the path info part of a URL. + * @see host + */ + public $pattern; + /** + * @var string the pattern used to parse and create the host info part of a URL (e.g. `http://example.com`). + * @see pattern + */ + public $host; + /** + * @var string the route to the controller action + */ + public $route; + /** + * @var array the default GET parameters (name => value) that this rule provides. + * When this rule is used to parse the incoming request, the values declared in this property + * will be injected into $_GET. + */ + public $defaults = []; + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * If not, the value of [[UrlManager::suffix]] will be used. + */ + public $suffix; + /** + * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * Use array to represent multiple verbs that this rule may match. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + */ + public $verb; + /** + * @var integer a value indicating if this rule should be used for both request parsing and URL creation, + * parsing only, or creation only. + * If not set or 0, it means the rule is both request parsing and URL creation. + * If it is [[PARSING_ONLY]], the rule is for request parsing only. + * If it is [[CREATION_ONLY]], the rule is for URL creation only. + */ + public $mode; - /** - * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. - */ - private $_template; - /** - * @var string the regex for matching the route part. This is used in generating URL. - */ - private $_routeRule; - /** - * @var array list of regex for matching parameters. This is used in generating URL. - */ - private $_paramRules = []; - /** - * @var array list of parameters used in the route. - */ - private $_routeParams = []; + /** + * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. + */ + private $_template; + /** + * @var string the regex for matching the route part. This is used in generating URL. + */ + private $_routeRule; + /** + * @var array list of regex for matching parameters. This is used in generating URL. + */ + private $_paramRules = []; + /** + * @var array list of parameters used in the route. + */ + private $_routeParams = []; - /** - * Initializes this rule. - */ - public function init() - { - if ($this->pattern === null) { - throw new InvalidConfigException('UrlRule::pattern must be set.'); - } - if ($this->route === null) { - throw new InvalidConfigException('UrlRule::route must be set.'); - } - if ($this->verb !== null) { - if (is_array($this->verb)) { - foreach ($this->verb as $i => $verb) { - $this->verb[$i] = strtoupper($verb); - } - } else { - $this->verb = [strtoupper($this->verb)]; - } - } - if ($this->name === null) { - $this->name = $this->pattern; - } + /** + * Initializes this rule. + */ + public function init() + { + if ($this->pattern === null) { + throw new InvalidConfigException('UrlRule::pattern must be set.'); + } + if ($this->route === null) { + throw new InvalidConfigException('UrlRule::route must be set.'); + } + if ($this->verb !== null) { + if (is_array($this->verb)) { + foreach ($this->verb as $i => $verb) { + $this->verb[$i] = strtoupper($verb); + } + } else { + $this->verb = [strtoupper($this->verb)]; + } + } + if ($this->name === null) { + $this->name = $this->pattern; + } - $this->pattern = trim($this->pattern, '/'); + $this->pattern = trim($this->pattern, '/'); - if ($this->host !== null) { - $this->host = rtrim($this->host, '/'); - $this->pattern = rtrim($this->host . '/' . $this->pattern, '/'); - } elseif ($this->pattern === '') { - $this->_template = ''; - $this->pattern = '#^$#u'; - return; - } elseif (($pos = strpos($this->pattern, '://')) !== false) { - if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) { - $this->host = substr($this->pattern, 0, $pos2); - } else { - $this->host = $this->pattern; - } - } else { - $this->pattern = '/' . $this->pattern . '/'; - } + if ($this->host !== null) { + $this->host = rtrim($this->host, '/'); + $this->pattern = rtrim($this->host . '/' . $this->pattern, '/'); + } elseif ($this->pattern === '') { + $this->_template = ''; + $this->pattern = '#^$#u'; - $this->route = trim($this->route, '/'); - if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { - foreach ($matches[1] as $name) { - $this->_routeParams[$name] = "<$name>"; - } - } + return; + } elseif (($pos = strpos($this->pattern, '://')) !== false) { + if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) { + $this->host = substr($this->pattern, 0, $pos2); + } else { + $this->host = $this->pattern; + } + } else { + $this->pattern = '/' . $this->pattern . '/'; + } - $tr = [ - '.' => '\\.', - '*' => '\\*', - '$' => '\\$', - '[' => '\\[', - ']' => '\\]', - '(' => '\\(', - ')' => '\\)', - ]; - $tr2 = []; - if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { - foreach ($matches as $match) { - $name = $match[1][0]; - $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+'; - if (array_key_exists($name, $this->defaults)) { - $length = strlen($match[0][0]); - $offset = $match[0][1]; - if ($offset > 1 && $this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { - $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; - } else { - $tr["<$name>"] = "(?P<$name>$pattern)?"; - } - } else { - $tr["<$name>"] = "(?P<$name>$pattern)"; - } - if (isset($this->_routeParams[$name])) { - $tr2["<$name>"] = "(?P<$name>$pattern)"; - } else { - $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; - } - } - } + $this->route = trim($this->route, '/'); + if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { + foreach ($matches[1] as $name) { + $this->_routeParams[$name] = "<$name>"; + } + } - $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); - $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; + $tr = [ + '.' => '\\.', + '*' => '\\*', + '$' => '\\$', + '[' => '\\[', + ']' => '\\]', + '(' => '\\(', + ')' => '\\)', + ]; + $tr2 = []; + if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + foreach ($matches as $match) { + $name = $match[1][0]; + $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+'; + if (array_key_exists($name, $this->defaults)) { + $length = strlen($match[0][0]); + $offset = $match[0][1]; + if ($offset > 1 && $this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { + $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; + } else { + $tr["<$name>"] = "(?P<$name>$pattern)?"; + } + } else { + $tr["<$name>"] = "(?P<$name>$pattern)"; + } + if (isset($this->_routeParams[$name])) { + $tr2["<$name>"] = "(?P<$name>$pattern)"; + } else { + $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; + } + } + } - if (!empty($this->_routeParams)) { - $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; - } - } + $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; - /** - * Parses the given request and returns the corresponding route and parameters. - * @param UrlManager $manager the URL manager - * @param Request $request the request component - * @return array|boolean the parsing result. The route and the parameters are returned as an array. - * If false, it means this rule cannot be used to parse this path info. - */ - public function parseRequest($manager, $request) - { - if ($this->mode === self::CREATION_ONLY) { - return false; - } + if (!empty($this->_routeParams)) { + $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; + } + } - if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) { - return false; - } + /** + * Parses the given request and returns the corresponding route and parameters. + * @param UrlManager $manager the URL manager + * @param Request $request the request component + * @return array|boolean the parsing result. The route and the parameters are returned as an array. + * If false, it means this rule cannot be used to parse this path info. + */ + public function parseRequest($manager, $request) + { + if ($this->mode === self::CREATION_ONLY) { + return false; + } - $pathInfo = $request->getPathInfo(); - $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix); - if ($suffix !== '' && $pathInfo !== '') { - $n = strlen($suffix); - if (substr($pathInfo, -$n) === $suffix) { - $pathInfo = substr($pathInfo, 0, -$n); - if ($pathInfo === '') { - // suffix alone is not allowed - return false; - } - } else { - return false; - } - } + if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) { + return false; + } - if ($this->host !== null) { - $pathInfo = strtolower($request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo); - } + $pathInfo = $request->getPathInfo(); + $suffix = (string) ($this->suffix === null ? $manager->suffix : $this->suffix); + if ($suffix !== '' && $pathInfo !== '') { + $n = strlen($suffix); + if (substr($pathInfo, -$n) === $suffix) { + $pathInfo = substr($pathInfo, 0, -$n); + if ($pathInfo === '') { + // suffix alone is not allowed + return false; + } + } else { + return false; + } + } - if (!preg_match($this->pattern, $pathInfo, $matches)) { - return false; - } - foreach ($this->defaults as $name => $value) { - if (!isset($matches[$name]) || $matches[$name] === '') { - $matches[$name] = $value; - } - } - $params = $this->defaults; - $tr = []; - foreach ($matches as $name => $value) { - if (isset($this->_routeParams[$name])) { - $tr[$this->_routeParams[$name]] = $value; - unset($params[$name]); - } elseif (isset($this->_paramRules[$name])) { - $params[$name] = $value; - } - } - if ($this->_routeRule !== null) { - $route = strtr($this->route, $tr); - } else { - $route = $this->route; - } + if ($this->host !== null) { + $pathInfo = strtolower($request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo); + } - Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__); + if (!preg_match($this->pattern, $pathInfo, $matches)) { + return false; + } + foreach ($this->defaults as $name => $value) { + if (!isset($matches[$name]) || $matches[$name] === '') { + $matches[$name] = $value; + } + } + $params = $this->defaults; + $tr = []; + foreach ($matches as $name => $value) { + if (isset($this->_routeParams[$name])) { + $tr[$this->_routeParams[$name]] = $value; + unset($params[$name]); + } elseif (isset($this->_paramRules[$name])) { + $params[$name] = $value; + } + } + if ($this->_routeRule !== null) { + $route = strtr($this->route, $tr); + } else { + $route = $this->route; + } - return [$route, $params]; - } + Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__); - /** - * Creates a URL according to the given route and parameters. - * @param UrlManager $manager the URL manager - * @param string $route the route. It should not have slashes at the beginning or the end. - * @param array $params the parameters - * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. - */ - public function createUrl($manager, $route, $params) - { - if ($this->mode === self::PARSING_ONLY) { - return false; - } + return [$route, $params]; + } - $tr = []; + /** + * Creates a URL according to the given route and parameters. + * @param UrlManager $manager the URL manager + * @param string $route the route. It should not have slashes at the beginning or the end. + * @param array $params the parameters + * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. + */ + public function createUrl($manager, $route, $params) + { + if ($this->mode === self::PARSING_ONLY) { + return false; + } - // match the route part first - if ($route !== $this->route) { - if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) { - foreach ($this->_routeParams as $name => $token) { - if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { - $tr[$token] = ''; - } else { - $tr[$token] = $matches[$name]; - } - } - } else { - return false; - } - } + $tr = []; - // match default params - // if a default param is not in the route pattern, its value must also be matched - foreach ($this->defaults as $name => $value) { - if (isset($this->_routeParams[$name])) { - continue; - } - if (!isset($params[$name])) { - return false; - } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically - unset($params[$name]); - if (isset($this->_paramRules[$name])) { - $tr["<$name>"] = ''; - } - } elseif (!isset($this->_paramRules[$name])) { - return false; - } - } + // match the route part first + if ($route !== $this->route) { + if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) { + foreach ($this->_routeParams as $name => $token) { + if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { + $tr[$token] = ''; + } else { + $tr[$token] = $matches[$name]; + } + } + } else { + return false; + } + } - // match params in the pattern - foreach ($this->_paramRules as $name => $rule) { - if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { - $tr["<$name>"] = urlencode($params[$name]); - unset($params[$name]); - } elseif (!isset($this->defaults[$name]) || isset($params[$name])) { - return false; - } - } + // match default params + // if a default param is not in the route pattern, its value must also be matched + foreach ($this->defaults as $name => $value) { + if (isset($this->_routeParams[$name])) { + continue; + } + if (!isset($params[$name])) { + return false; + } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically + unset($params[$name]); + if (isset($this->_paramRules[$name])) { + $tr["<$name>"] = ''; + } + } elseif (!isset($this->_paramRules[$name])) { + return false; + } + } - $url = trim(strtr($this->_template, $tr), '/'); - if ($this->host !== null) { - $pos = strpos($url, '/', 8); - if ($pos !== false) { - $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos)); - } - } elseif (strpos($url, '//') !== false) { - $url = preg_replace('#/+#', '/', $url); - } + // match params in the pattern + foreach ($this->_paramRules as $name => $rule) { + if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { + $tr["<$name>"] = urlencode($params[$name]); + unset($params[$name]); + } elseif (!isset($this->defaults[$name]) || isset($params[$name])) { + return false; + } + } - if ($url !== '') { - $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); - } + $url = trim(strtr($this->_template, $tr), '/'); + if ($this->host !== null) { + $pos = strpos($url, '/', 8); + if ($pos !== false) { + $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos)); + } + } elseif (strpos($url, '//') !== false) { + $url = preg_replace('#/+#', '/', $url); + } - if (!empty($params) && ($query = http_build_query($params)) !== '') { - $url .= '?' . $query; - } - return $url; - } + if ($url !== '') { + $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); + } + + if (!empty($params) && ($query = http_build_query($params)) !== '') { + $url .= '?' . $query; + } + + return $url; + } } diff --git a/framework/web/UrlRuleInterface.php b/framework/web/UrlRuleInterface.php index e6a53859604..9063c7732a9 100644 --- a/framework/web/UrlRuleInterface.php +++ b/framework/web/UrlRuleInterface.php @@ -15,20 +15,20 @@ */ interface UrlRuleInterface { - /** - * Parses the given request and returns the corresponding route and parameters. - * @param UrlManager $manager the URL manager - * @param Request $request the request component - * @return array|boolean the parsing result. The route and the parameters are returned as an array. - * If false, it means this rule cannot be used to parse this path info. - */ - public function parseRequest($manager, $request); - /** - * Creates a URL according to the given route and parameters. - * @param UrlManager $manager the URL manager - * @param string $route the route. It should not have slashes at the beginning or the end. - * @param array $params the parameters - * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. - */ - public function createUrl($manager, $route, $params); + /** + * Parses the given request and returns the corresponding route and parameters. + * @param UrlManager $manager the URL manager + * @param Request $request the request component + * @return array|boolean the parsing result. The route and the parameters are returned as an array. + * If false, it means this rule cannot be used to parse this path info. + */ + public function parseRequest($manager, $request); + /** + * Creates a URL according to the given route and parameters. + * @param UrlManager $manager the URL manager + * @param string $route the route. It should not have slashes at the beginning or the end. + * @param array $params the parameters + * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. + */ + public function createUrl($manager, $route, $params); } diff --git a/framework/web/User.php b/framework/web/User.php index cc0e5a7facd..0f51f57470f 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -57,532 +57,540 @@ */ class User extends Component { - const EVENT_BEFORE_LOGIN = 'beforeLogin'; - const EVENT_AFTER_LOGIN = 'afterLogin'; - const EVENT_BEFORE_LOGOUT = 'beforeLogout'; - const EVENT_AFTER_LOGOUT = 'afterLogout'; - - /** - * @var string the class name of the [[identity]] object. - */ - public $identityClass; - /** - * @var boolean whether to enable cookie-based login. Defaults to false. - */ - public $enableAutoLogin = false; - /** - * @var string|array the URL for login when [[loginRequired()]] is called. - * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. - * The first element of the array should be the route to the login action, and the rest of - * the name-value pairs are GET parameters used to construct the login URL. For example, - * - * ~~~ - * ['site/login', 'ref' => 1] - * ~~~ - * - * If this property is null, a 403 HTTP exception will be raised when [[loginRequired()]] is called. - */ - public $loginUrl = ['site/login']; - /** - * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. - * @see Cookie - */ - public $identityCookie = ['name' => '_identity', 'httpOnly' => true]; - /** - * @var integer the number of seconds in which the user will be logged out automatically if he - * remains inactive. If this property is not set, the user will be logged out after - * the current session expires (c.f. [[Session::timeout]]). - */ - public $authTimeout; - /** - * @var boolean whether to automatically renew the identity cookie each time a page is requested. - * This property is effective only when [[enableAutoLogin]] is true. - * When this is false, the identity cookie will expire after the specified duration since the user - * is initially logged in. When this is true, the identity cookie will expire after the specified duration - * since the user visits the site the last time. - * @see enableAutoLogin - */ - public $autoRenewCookie = true; - /** - * @var string the session variable name used to store the value of [[id]]. - */ - public $idParam = '__id'; - /** - * @var string the session variable name used to store the value of expiration timestamp of the authenticated state. - * This is used when [[authTimeout]] is set. - */ - public $authTimeoutParam = '__expire'; - /** - * @var string the session variable name used to store the value of [[returnUrl]]. - */ - public $returnUrlParam = '__returnUrl'; - - private $_access = []; - - - /** - * Initializes the application component. - */ - public function init() - { - parent::init(); - - if ($this->identityClass === null) { - throw new InvalidConfigException('User::identityClass must be set.'); - } - if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { - throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); - } - } - - private $_identity = false; - - /** - * Returns the identity object associated with the currently logged-in user. - * @param boolean $checkSession whether to check the session if the identity has never been determined before. - * If the identity is already determined (e.g., by calling [[setIdentity()]] or [[login()]]), - * then this parameter has no effect. - * @return IdentityInterface the identity object associated with the currently logged-in user. - * Null is returned if the user is not logged in (not authenticated). - * @see login() - * @see logout() - */ - public function getIdentity($checkSession = true) - { - if ($this->_identity === false) { - if ($checkSession) { - $this->renewAuthStatus(); - } else { - return null; - } - } - return $this->_identity; - } - - /** - * Sets the user identity object. - * - * This method does nothing else except storing the specified identity object in the internal variable. - * For this reason, this method is best used when the user authentication status should not be maintained - * by session. - * - * This method is also called by other more sophisticated methods, such as [[login()]], [[logout()]], - * [[switchIdentity()]]. Those methods will try to use session and cookie to maintain the user authentication - * status. - * - * @param IdentityInterface $identity the identity object associated with the currently logged user. - */ - public function setIdentity($identity) - { - $this->_identity = $identity; - $this->_access = []; - } - - /** - * Logs in a user. - * - * By logging in a user, you may obtain the user identity information each time through [[identity]]. - * - * The login status is maintained according to the `$duration` parameter: - * - * - `$duration == 0`: the identity information will be stored in session and will be available - * via [[identity]] as long as the session remains active. - * - `$duration > 0`: the identity information will be stored in session. If [[enableAutoLogin]] is true, - * it will also be stored in a cookie which will expire in `$duration` seconds. As long as - * the cookie remains valid or the session is active, you may obtain the user identity information - * via [[identity]]. - * - * @param IdentityInterface $identity the user identity (which should already be authenticated) - * @param integer $duration number of seconds that the user can remain in logged-in status. - * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed. - * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. - * @return boolean whether the user is logged in - */ - public function login($identity, $duration = 0) - { - if ($this->beforeLogin($identity, false, $duration)) { - $this->switchIdentity($identity, $duration); - $id = $identity->getId(); - $ip = Yii::$app->getRequest()->getUserIP(); - Yii::info("User '$id' logged in from $ip with duration $duration.", __METHOD__); - $this->afterLogin($identity, false, $duration); - } - return !$this->getIsGuest(); - } - - /** - * Logs in a user by the given access token. - * Note that unlike [[login()]], this method will NOT start a session to remember the user authentication status. - * Also if the access token is invalid, the user will remain as a guest. - * @param string $token the access token - * @return IdentityInterface the identity associated with the given access token. Null is returned if - * the access token is invalid. - */ - public function loginByAccessToken($token) - { - /** @var IdentityInterface $class */ - $class = $this->identityClass; - $identity = $class::findIdentityByAccessToken($token); - $this->setIdentity($identity); - return $identity; - } - - /** - * Logs in a user by cookie. - * - * This method attempts to log in a user using the ID and authKey information - * provided by the given cookie. - */ - protected function loginByCookie() - { - $name = $this->identityCookie['name']; - $value = Yii::$app->getRequest()->getCookies()->getValue($name); - if ($value !== null) { - $data = json_decode($value, true); - if (count($data) === 3 && isset($data[0], $data[1], $data[2])) { - list ($id, $authKey, $duration) = $data; - /** @var IdentityInterface $class */ - $class = $this->identityClass; - $identity = $class::findIdentity($id); - if ($identity !== null && $identity->validateAuthKey($authKey)) { - if ($this->beforeLogin($identity, true, $duration)) { - $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); - $ip = Yii::$app->getRequest()->getUserIP(); - Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); - $this->afterLogin($identity, true, $duration); - } - } elseif ($identity !== null) { - Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); - } - } - } - } - - /** - * Logs out the current user. - * This will remove authentication-related session data. - * If `$destroySession` is true, all session data will be removed. - * @param boolean $destroySession whether to destroy the whole session. Defaults to true. - */ - public function logout($destroySession = true) - { - $identity = $this->getIdentity(); - if ($identity !== null && $this->beforeLogout($identity)) { - $this->switchIdentity(null); - $id = $identity->getId(); - $ip = Yii::$app->getRequest()->getUserIP(); - Yii::info("User '$id' logged out from $ip.", __METHOD__); - if ($destroySession) { - Yii::$app->getSession()->destroy(); - } - $this->afterLogout($identity); - } - } - - /** - * Returns a value indicating whether the user is a guest (not authenticated). - * @return boolean whether the current user is a guest. - */ - public function getIsGuest() - { - return $this->getIdentity() === null; - } - - /** - * Returns a value that uniquely represents the user. - * @return string|integer the unique identifier for the user. If null, it means the user is a guest. - */ - public function getId() - { - $identity = $this->getIdentity(); - return $identity !== null ? $identity->getId() : null; - } - - /** - * Returns the URL that the user should be redirected to after successful login. - * This property is usually used by the login action. If the login is successful, - * the action should read this property and use it to redirect the user browser. - * @param string|array $defaultUrl the default return URL in case it was not set previously. - * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. - * Please refer to [[setReturnUrl()]] on accepted format of the URL. - * @return string the URL that the user should be redirected to after login. - * @see loginRequired() - */ - public function getReturnUrl($defaultUrl = null) - { - $url = Yii::$app->getSession()->get($this->returnUrlParam, $defaultUrl); - if (is_array($url)) { - if (isset($url[0])) { - $route = array_shift($url); - return Yii::$app->getUrlManager()->createUrl($route, $url); - } else { - $url = null; - } - } - return $url === null ? Yii::$app->getHomeUrl() : $url; - } - - /** - * @param string|array $url the URL that the user should be redirected to after login. - * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. - * The first element of the array should be the route, and the rest of - * the name-value pairs are GET parameters used to construct the URL. For example, - * - * ~~~ - * ['admin/index', 'ref' => 1] - * ~~~ - */ - public function setReturnUrl($url) - { - Yii::$app->getSession()->set($this->returnUrlParam, $url); - } - - /** - * Redirects the user browser to the login page. - * Before the redirection, the current URL (if it's not an AJAX url) will be - * kept as [[returnUrl]] so that the user browser may be redirected back - * to the current page after successful login. Make sure you set [[loginUrl]] - * so that the user browser can be redirected to the specified login URL after - * calling this method. - * - * Note that when [[loginUrl]] is set, calling this method will NOT terminate the application execution. - * - * @return Response the redirection response if [[loginUrl]] is set - * @throws ForbiddenHttpException the "Access Denied" HTTP exception if [[loginUrl]] is not set - */ - public function loginRequired() - { - $request = Yii::$app->getRequest(); - if (!$request->getIsAjax()) { - $this->setReturnUrl($request->getUrl()); - } - if ($this->loginUrl !== null) { - return Yii::$app->getResponse()->redirect($this->loginUrl); - } else { - throw new ForbiddenHttpException(Yii::t('yii', 'Login Required')); - } - } - - /** - * This method is called before logging in a user. - * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event. - * If you override this method, make sure you call the parent implementation - * so that the event is triggered. - * @param IdentityInterface $identity the user identity information - * @param boolean $cookieBased whether the login is cookie-based - * @param integer $duration number of seconds that the user can remain in logged-in status. - * If 0, it means login till the user closes the browser or the session is manually destroyed. - * @return boolean whether the user should continue to be logged in - */ - protected function beforeLogin($identity, $cookieBased, $duration) - { - $event = new UserEvent([ - 'identity' => $identity, - 'cookieBased' => $cookieBased, - 'duration' => $duration, - ]); - $this->trigger(self::EVENT_BEFORE_LOGIN, $event); - return $event->isValid; - } - - /** - * This method is called after the user is successfully logged in. - * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event. - * If you override this method, make sure you call the parent implementation - * so that the event is triggered. - * @param IdentityInterface $identity the user identity information - * @param boolean $cookieBased whether the login is cookie-based - * @param integer $duration number of seconds that the user can remain in logged-in status. - * If 0, it means login till the user closes the browser or the session is manually destroyed. - */ - protected function afterLogin($identity, $cookieBased, $duration) - { - $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([ - 'identity' => $identity, - 'cookieBased' => $cookieBased, - 'duration' => $duration, - ])); - } - - /** - * This method is invoked when calling [[logout()]] to log out a user. - * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event. - * If you override this method, make sure you call the parent implementation - * so that the event is triggered. - * @param IdentityInterface $identity the user identity information - * @return boolean whether the user should continue to be logged out - */ - protected function beforeLogout($identity) - { - $event = new UserEvent([ - 'identity' => $identity, - ]); - $this->trigger(self::EVENT_BEFORE_LOGOUT, $event); - return $event->isValid; - } - - /** - * This method is invoked right after a user is logged out via [[logout()]]. - * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event. - * If you override this method, make sure you call the parent implementation - * so that the event is triggered. - * @param IdentityInterface $identity the user identity information - */ - protected function afterLogout($identity) - { - $this->trigger(self::EVENT_AFTER_LOGOUT, new UserEvent([ - 'identity' => $identity, - ])); - } - - /** - * Renews the identity cookie. - * This method will set the expiration time of the identity cookie to be the current time - * plus the originally specified cookie duration. - */ - protected function renewIdentityCookie() - { - $name = $this->identityCookie['name']; - $value = Yii::$app->getRequest()->getCookies()->getValue($name); - if ($value !== null) { - $data = json_decode($value, true); - if (is_array($data) && isset($data[2])) { - $cookie = new Cookie($this->identityCookie); - $cookie->value = $value; - $cookie->expire = time() + (int)$data[2]; - Yii::$app->getResponse()->getCookies()->add($cookie); - } - } - } - - /** - * Sends an identity cookie. - * This method is used when [[enableAutoLogin]] is true. - * It saves [[id]], [[IdentityInterface::getAuthKey()|auth key]], and the duration of cookie-based login - * information in the cookie. - * @param IdentityInterface $identity - * @param integer $duration number of seconds that the user can remain in logged-in status. - * @see loginByCookie() - */ - protected function sendIdentityCookie($identity, $duration) - { - $cookie = new Cookie($this->identityCookie); - $cookie->value = json_encode([ - $identity->getId(), - $identity->getAuthKey(), - $duration, - ]); - $cookie->expire = time() + $duration; - Yii::$app->getResponse()->getCookies()->add($cookie); - } - - /** - * Switches to a new identity for the current user. - * - * This method may use session and/or cookie to store the user identity information, - * according to the value of `$duration`. Please refer to [[login()]] for more details. - * - * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] - * when the current user needs to be associated with the corresponding identity information. - * - * @param IdentityInterface $identity the identity information to be associated with the current user. - * If null, it means switching the current user to be a guest. - * @param integer $duration number of seconds that the user can remain in logged-in status. - * This parameter is used only when `$identity` is not null. - */ - public function switchIdentity($identity, $duration = 0) - { - $session = Yii::$app->getSession(); - if (!YII_ENV_TEST) { - $session->regenerateID(true); - } - $this->setIdentity($identity); - $session->remove($this->idParam); - $session->remove($this->authTimeoutParam); - if ($identity instanceof IdentityInterface) { - $session->set($this->idParam, $identity->getId()); - if ($this->authTimeout !== null) { - $session->set($this->authTimeoutParam, time() + $this->authTimeout); - } - if ($duration > 0 && $this->enableAutoLogin) { - $this->sendIdentityCookie($identity, $duration); - } - } elseif ($this->enableAutoLogin) { - Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); - } - } - - /** - * Updates the authentication status using the information from session and cookie. - * - * This method will try to determine the user identity using the [[idParam]] session variable. - * - * If [[authTimeout]] is set, this method will refresh the timer. - * - * If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]] - * if [[enableAutoLogin]] is true. - */ - protected function renewAuthStatus() - { - $session = Yii::$app->getSession(); - $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null; - - if ($id === null) { - $identity = null; - } else { - /** @var IdentityInterface $class */ - $class = $this->identityClass; - $identity = $class::findIdentity($id); - } - - $this->setIdentity($identity); - - if ($this->authTimeout !== null && $identity !== null) { - $expire = $session->get($this->authTimeoutParam); - if ($expire !== null && $expire < time()) { - $this->logout(false); - } else { - $session->set($this->authTimeoutParam, time() + $this->authTimeout); - } - } - - if ($this->enableAutoLogin) { - if ($this->getIsGuest()) { - $this->loginByCookie(); - } elseif ($this->autoRenewCookie) { - $this->renewIdentityCookie(); - } - } - } - - /** - * Performs access check for this user. - * @param string $operation the name of the operation that need access check. - * @param array $params name-value pairs that would be passed to business rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to - * this array, which holds the value of [[id]] when [[DbAuthManager]] or - * [[PhpAuthManager]] is used. - * @param boolean $allowCaching whether to allow caching the result of access check. - * When this parameter is true (default), if the access check of an operation was performed - * before, its result will be directly returned when calling this method to check the same - * operation. If this parameter is false, this method will always call - * [[AuthManager::checkAccess()]] to obtain the up-to-date access result. Note that this - * caching is effective only within the same request and only works when `$params = []`. - * @return boolean whether the operations can be performed by this user. - */ - public function checkAccess($operation, $params = [], $allowCaching = true) - { - $auth = Yii::$app->getAuthManager(); - if ($auth === null) { - return false; - } - if ($allowCaching && empty($params) && isset($this->_access[$operation])) { - return $this->_access[$operation]; - } - $access = $auth->checkAccess($this->getId(), $operation, $params); - if ($allowCaching && empty($params)) { - $this->_access[$operation] = $access; - } - return $access; - } + const EVENT_BEFORE_LOGIN = 'beforeLogin'; + const EVENT_AFTER_LOGIN = 'afterLogin'; + const EVENT_BEFORE_LOGOUT = 'beforeLogout'; + const EVENT_AFTER_LOGOUT = 'afterLogout'; + + /** + * @var string the class name of the [[identity]] object. + */ + public $identityClass; + /** + * @var boolean whether to enable cookie-based login. Defaults to false. + */ + public $enableAutoLogin = false; + /** + * @var string|array the URL for login when [[loginRequired()]] is called. + * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. + * The first element of the array should be the route to the login action, and the rest of + * the name-value pairs are GET parameters used to construct the login URL. For example, + * + * ~~~ + * ['site/login', 'ref' => 1] + * ~~~ + * + * If this property is null, a 403 HTTP exception will be raised when [[loginRequired()]] is called. + */ + public $loginUrl = ['site/login']; + /** + * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. + * @see Cookie + */ + public $identityCookie = ['name' => '_identity', 'httpOnly' => true]; + /** + * @var integer the number of seconds in which the user will be logged out automatically if he + * remains inactive. If this property is not set, the user will be logged out after + * the current session expires (c.f. [[Session::timeout]]). + */ + public $authTimeout; + /** + * @var boolean whether to automatically renew the identity cookie each time a page is requested. + * This property is effective only when [[enableAutoLogin]] is true. + * When this is false, the identity cookie will expire after the specified duration since the user + * is initially logged in. When this is true, the identity cookie will expire after the specified duration + * since the user visits the site the last time. + * @see enableAutoLogin + */ + public $autoRenewCookie = true; + /** + * @var string the session variable name used to store the value of [[id]]. + */ + public $idParam = '__id'; + /** + * @var string the session variable name used to store the value of expiration timestamp of the authenticated state. + * This is used when [[authTimeout]] is set. + */ + public $authTimeoutParam = '__expire'; + /** + * @var string the session variable name used to store the value of [[returnUrl]]. + */ + public $returnUrlParam = '__returnUrl'; + + private $_access = []; + + /** + * Initializes the application component. + */ + public function init() + { + parent::init(); + + if ($this->identityClass === null) { + throw new InvalidConfigException('User::identityClass must be set.'); + } + if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { + throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); + } + } + + private $_identity = false; + + /** + * Returns the identity object associated with the currently logged-in user. + * @param boolean $checkSession whether to check the session if the identity has never been determined before. + * If the identity is already determined (e.g., by calling [[setIdentity()]] or [[login()]]), + * then this parameter has no effect. + * @return IdentityInterface the identity object associated with the currently logged-in user. + * Null is returned if the user is not logged in (not authenticated). + * @see login() + * @see logout() + */ + public function getIdentity($checkSession = true) + { + if ($this->_identity === false) { + if ($checkSession) { + $this->renewAuthStatus(); + } else { + return null; + } + } + + return $this->_identity; + } + + /** + * Sets the user identity object. + * + * This method does nothing else except storing the specified identity object in the internal variable. + * For this reason, this method is best used when the user authentication status should not be maintained + * by session. + * + * This method is also called by other more sophisticated methods, such as [[login()]], [[logout()]], + * [[switchIdentity()]]. Those methods will try to use session and cookie to maintain the user authentication + * status. + * + * @param IdentityInterface $identity the identity object associated with the currently logged user. + */ + public function setIdentity($identity) + { + $this->_identity = $identity; + $this->_access = []; + } + + /** + * Logs in a user. + * + * By logging in a user, you may obtain the user identity information each time through [[identity]]. + * + * The login status is maintained according to the `$duration` parameter: + * + * - `$duration == 0`: the identity information will be stored in session and will be available + * via [[identity]] as long as the session remains active. + * - `$duration > 0`: the identity information will be stored in session. If [[enableAutoLogin]] is true, + * it will also be stored in a cookie which will expire in `$duration` seconds. As long as + * the cookie remains valid or the session is active, you may obtain the user identity information + * via [[identity]]. + * + * @param IdentityInterface $identity the user identity (which should already be authenticated) + * @param integer $duration number of seconds that the user can remain in logged-in status. + * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed. + * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. + * @return boolean whether the user is logged in + */ + public function login($identity, $duration = 0) + { + if ($this->beforeLogin($identity, false, $duration)) { + $this->switchIdentity($identity, $duration); + $id = $identity->getId(); + $ip = Yii::$app->getRequest()->getUserIP(); + Yii::info("User '$id' logged in from $ip with duration $duration.", __METHOD__); + $this->afterLogin($identity, false, $duration); + } + + return !$this->getIsGuest(); + } + + /** + * Logs in a user by the given access token. + * Note that unlike [[login()]], this method will NOT start a session to remember the user authentication status. + * Also if the access token is invalid, the user will remain as a guest. + * @param string $token the access token + * @return IdentityInterface the identity associated with the given access token. Null is returned if + * the access token is invalid. + */ + public function loginByAccessToken($token) + { + /** @var IdentityInterface $class */ + $class = $this->identityClass; + $identity = $class::findIdentityByAccessToken($token); + $this->setIdentity($identity); + + return $identity; + } + + /** + * Logs in a user by cookie. + * + * This method attempts to log in a user using the ID and authKey information + * provided by the given cookie. + */ + protected function loginByCookie() + { + $name = $this->identityCookie['name']; + $value = Yii::$app->getRequest()->getCookies()->getValue($name); + if ($value !== null) { + $data = json_decode($value, true); + if (count($data) === 3 && isset($data[0], $data[1], $data[2])) { + list ($id, $authKey, $duration) = $data; + /** @var IdentityInterface $class */ + $class = $this->identityClass; + $identity = $class::findIdentity($id); + if ($identity !== null && $identity->validateAuthKey($authKey)) { + if ($this->beforeLogin($identity, true, $duration)) { + $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); + $ip = Yii::$app->getRequest()->getUserIP(); + Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__); + $this->afterLogin($identity, true, $duration); + } + } elseif ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); + } + } + } + } + + /** + * Logs out the current user. + * This will remove authentication-related session data. + * If `$destroySession` is true, all session data will be removed. + * @param boolean $destroySession whether to destroy the whole session. Defaults to true. + */ + public function logout($destroySession = true) + { + $identity = $this->getIdentity(); + if ($identity !== null && $this->beforeLogout($identity)) { + $this->switchIdentity(null); + $id = $identity->getId(); + $ip = Yii::$app->getRequest()->getUserIP(); + Yii::info("User '$id' logged out from $ip.", __METHOD__); + if ($destroySession) { + Yii::$app->getSession()->destroy(); + } + $this->afterLogout($identity); + } + } + + /** + * Returns a value indicating whether the user is a guest (not authenticated). + * @return boolean whether the current user is a guest. + */ + public function getIsGuest() + { + return $this->getIdentity() === null; + } + + /** + * Returns a value that uniquely represents the user. + * @return string|integer the unique identifier for the user. If null, it means the user is a guest. + */ + public function getId() + { + $identity = $this->getIdentity(); + + return $identity !== null ? $identity->getId() : null; + } + + /** + * Returns the URL that the user should be redirected to after successful login. + * This property is usually used by the login action. If the login is successful, + * the action should read this property and use it to redirect the user browser. + * @param string|array $defaultUrl the default return URL in case it was not set previously. + * If this is null and the return URL was not set previously, [[Application::homeUrl]] will be redirected to. + * Please refer to [[setReturnUrl()]] on accepted format of the URL. + * @return string the URL that the user should be redirected to after login. + * @see loginRequired() + */ + public function getReturnUrl($defaultUrl = null) + { + $url = Yii::$app->getSession()->get($this->returnUrlParam, $defaultUrl); + if (is_array($url)) { + if (isset($url[0])) { + $route = array_shift($url); + + return Yii::$app->getUrlManager()->createUrl($route, $url); + } else { + $url = null; + } + } + + return $url === null ? Yii::$app->getHomeUrl() : $url; + } + + /** + * @param string|array $url the URL that the user should be redirected to after login. + * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. + * The first element of the array should be the route, and the rest of + * the name-value pairs are GET parameters used to construct the URL. For example, + * + * ~~~ + * ['admin/index', 'ref' => 1] + * ~~~ + */ + public function setReturnUrl($url) + { + Yii::$app->getSession()->set($this->returnUrlParam, $url); + } + + /** + * Redirects the user browser to the login page. + * Before the redirection, the current URL (if it's not an AJAX url) will be + * kept as [[returnUrl]] so that the user browser may be redirected back + * to the current page after successful login. Make sure you set [[loginUrl]] + * so that the user browser can be redirected to the specified login URL after + * calling this method. + * + * Note that when [[loginUrl]] is set, calling this method will NOT terminate the application execution. + * + * @return Response the redirection response if [[loginUrl]] is set + * @throws ForbiddenHttpException the "Access Denied" HTTP exception if [[loginUrl]] is not set + */ + public function loginRequired() + { + $request = Yii::$app->getRequest(); + if (!$request->getIsAjax()) { + $this->setReturnUrl($request->getUrl()); + } + if ($this->loginUrl !== null) { + return Yii::$app->getResponse()->redirect($this->loginUrl); + } else { + throw new ForbiddenHttpException(Yii::t('yii', 'Login Required')); + } + } + + /** + * This method is called before logging in a user. + * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param IdentityInterface $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based + * @param integer $duration number of seconds that the user can remain in logged-in status. + * If 0, it means login till the user closes the browser or the session is manually destroyed. + * @return boolean whether the user should continue to be logged in + */ + protected function beforeLogin($identity, $cookieBased, $duration) + { + $event = new UserEvent([ + 'identity' => $identity, + 'cookieBased' => $cookieBased, + 'duration' => $duration, + ]); + $this->trigger(self::EVENT_BEFORE_LOGIN, $event); + + return $event->isValid; + } + + /** + * This method is called after the user is successfully logged in. + * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param IdentityInterface $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based + * @param integer $duration number of seconds that the user can remain in logged-in status. + * If 0, it means login till the user closes the browser or the session is manually destroyed. + */ + protected function afterLogin($identity, $cookieBased, $duration) + { + $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent([ + 'identity' => $identity, + 'cookieBased' => $cookieBased, + 'duration' => $duration, + ])); + } + + /** + * This method is invoked when calling [[logout()]] to log out a user. + * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param IdentityInterface $identity the user identity information + * @return boolean whether the user should continue to be logged out + */ + protected function beforeLogout($identity) + { + $event = new UserEvent([ + 'identity' => $identity, + ]); + $this->trigger(self::EVENT_BEFORE_LOGOUT, $event); + + return $event->isValid; + } + + /** + * This method is invoked right after a user is logged out via [[logout()]]. + * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param IdentityInterface $identity the user identity information + */ + protected function afterLogout($identity) + { + $this->trigger(self::EVENT_AFTER_LOGOUT, new UserEvent([ + 'identity' => $identity, + ])); + } + + /** + * Renews the identity cookie. + * This method will set the expiration time of the identity cookie to be the current time + * plus the originally specified cookie duration. + */ + protected function renewIdentityCookie() + { + $name = $this->identityCookie['name']; + $value = Yii::$app->getRequest()->getCookies()->getValue($name); + if ($value !== null) { + $data = json_decode($value, true); + if (is_array($data) && isset($data[2])) { + $cookie = new Cookie($this->identityCookie); + $cookie->value = $value; + $cookie->expire = time() + (int) $data[2]; + Yii::$app->getResponse()->getCookies()->add($cookie); + } + } + } + + /** + * Sends an identity cookie. + * This method is used when [[enableAutoLogin]] is true. + * It saves [[id]], [[IdentityInterface::getAuthKey()|auth key]], and the duration of cookie-based login + * information in the cookie. + * @param IdentityInterface $identity + * @param integer $duration number of seconds that the user can remain in logged-in status. + * @see loginByCookie() + */ + protected function sendIdentityCookie($identity, $duration) + { + $cookie = new Cookie($this->identityCookie); + $cookie->value = json_encode([ + $identity->getId(), + $identity->getAuthKey(), + $duration, + ]); + $cookie->expire = time() + $duration; + Yii::$app->getResponse()->getCookies()->add($cookie); + } + + /** + * Switches to a new identity for the current user. + * + * This method may use session and/or cookie to store the user identity information, + * according to the value of `$duration`. Please refer to [[login()]] for more details. + * + * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding identity information. + * + * @param IdentityInterface $identity the identity information to be associated with the current user. + * If null, it means switching the current user to be a guest. + * @param integer $duration number of seconds that the user can remain in logged-in status. + * This parameter is used only when `$identity` is not null. + */ + public function switchIdentity($identity, $duration = 0) + { + $session = Yii::$app->getSession(); + if (!YII_ENV_TEST) { + $session->regenerateID(true); + } + $this->setIdentity($identity); + $session->remove($this->idParam); + $session->remove($this->authTimeoutParam); + if ($identity instanceof IdentityInterface) { + $session->set($this->idParam, $identity->getId()); + if ($this->authTimeout !== null) { + $session->set($this->authTimeoutParam, time() + $this->authTimeout); + } + if ($duration > 0 && $this->enableAutoLogin) { + $this->sendIdentityCookie($identity, $duration); + } + } elseif ($this->enableAutoLogin) { + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); + } + } + + /** + * Updates the authentication status using the information from session and cookie. + * + * This method will try to determine the user identity using the [[idParam]] session variable. + * + * If [[authTimeout]] is set, this method will refresh the timer. + * + * If the user identity cannot be determined by session, this method will try to [[loginByCookie()|login by cookie]] + * if [[enableAutoLogin]] is true. + */ + protected function renewAuthStatus() + { + $session = Yii::$app->getSession(); + $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null; + + if ($id === null) { + $identity = null; + } else { + /** @var IdentityInterface $class */ + $class = $this->identityClass; + $identity = $class::findIdentity($id); + } + + $this->setIdentity($identity); + + if ($this->authTimeout !== null && $identity !== null) { + $expire = $session->get($this->authTimeoutParam); + if ($expire !== null && $expire < time()) { + $this->logout(false); + } else { + $session->set($this->authTimeoutParam, time() + $this->authTimeout); + } + } + + if ($this->enableAutoLogin) { + if ($this->getIsGuest()) { + $this->loginByCookie(); + } elseif ($this->autoRenewCookie) { + $this->renewIdentityCookie(); + } + } + } + + /** + * Performs access check for this user. + * @param string $operation the name of the operation that need access check. + * @param array $params name-value pairs that would be passed to business rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of [[id]] when [[DbAuthManager]] or + * [[PhpAuthManager]] is used. + * @param boolean $allowCaching whether to allow caching the result of access check. + * When this parameter is true (default), if the access check of an operation was performed + * before, its result will be directly returned when calling this method to check the same + * operation. If this parameter is false, this method will always call + * [[AuthManager::checkAccess()]] to obtain the up-to-date access result. Note that this + * caching is effective only within the same request and only works when `$params = []`. + * @return boolean whether the operations can be performed by this user. + */ + public function checkAccess($operation, $params = [], $allowCaching = true) + { + $auth = Yii::$app->getAuthManager(); + if ($auth === null) { + return false; + } + if ($allowCaching && empty($params) && isset($this->_access[$operation])) { + return $this->_access[$operation]; + } + $access = $auth->checkAccess($this->getId(), $operation, $params); + if ($allowCaching && empty($params)) { + $this->_access[$operation] = $access; + } + + return $access; + } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index bc6d5fe0664..cc2e328b6e5 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -17,24 +17,24 @@ */ class UserEvent extends Event { - /** - * @var IdentityInterface the identity object associated with this event - */ - public $identity; - /** - * @var boolean whether the login is cookie-based. This property is only meaningful - * for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events. - */ - public $cookieBased; - /** - * @var integer $duration number of seconds that the user can remain in logged-in status. - * If 0, it means login till the user closes the browser or the session is manually destroyed. - */ - public $duration; - /** - * @var boolean whether the login or logout should proceed. - * Event handlers may modify this property to determine whether the login or logout should proceed. - * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. - */ - public $isValid = true; + /** + * @var IdentityInterface the identity object associated with this event + */ + public $identity; + /** + * @var boolean whether the login is cookie-based. This property is only meaningful + * for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events. + */ + public $cookieBased; + /** + * @var integer $duration number of seconds that the user can remain in logged-in status. + * If 0, it means login till the user closes the browser or the session is manually destroyed. + */ + public $duration; + /** + * @var boolean whether the login or logout should proceed. + * Event handlers may modify this property to determine whether the login or logout should proceed. + * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. + */ + public $isValid = true; } diff --git a/framework/web/VerbFilter.php b/framework/web/VerbFilter.php index 5841230b2a0..8b948e85f1c 100644 --- a/framework/web/VerbFilter.php +++ b/framework/web/VerbFilter.php @@ -45,64 +45,63 @@ */ class VerbFilter extends Behavior { - /** - * @var array this property defines the allowed request methods for each action. - * For each action that should only support limited set of request methods - * you add an entry with the action id as array key and an array of - * allowed methods (e.g. GET, HEAD, PUT) as the value. - * If an action is not listed all request methods are considered allowed. - * - * You can use '*' to stand for all actions. When an action is explicitly - * specified, it takes precedence over the specification given by '*'. - * - * For example, - * - * ~~~ - * [ - * 'create' => ['get', 'post'], - * 'update' => ['get', 'put', 'post'], - * 'delete' => ['post', 'delete'], - * '*' => ['get'], - * ] - * ~~~ - */ - public $actions = []; + /** + * @var array this property defines the allowed request methods for each action. + * For each action that should only support limited set of request methods + * you add an entry with the action id as array key and an array of + * allowed methods (e.g. GET, HEAD, PUT) as the value. + * If an action is not listed all request methods are considered allowed. + * + * You can use '*' to stand for all actions. When an action is explicitly + * specified, it takes precedence over the specification given by '*'. + * + * For example, + * + * ~~~ + * [ + * 'create' => ['get', 'post'], + * 'update' => ['get', 'put', 'post'], + * 'delete' => ['post', 'delete'], + * '*' => ['get'], + * ] + * ~~~ + */ + public $actions = []; + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return [Controller::EVENT_BEFORE_ACTION => 'beforeAction']; + } - /** - * Declares event handlers for the [[owner]]'s events. - * @return array events (array keys) and the corresponding event handler methods (array values). - */ - public function events() - { - return [Controller::EVENT_BEFORE_ACTION => 'beforeAction']; - } + /** + * @param ActionEvent $event + * @return boolean + * @throws HttpException when the request method is not allowed. + */ + public function beforeAction($event) + { + $action = $event->action->id; + if (isset($this->actions[$action])) { + $verbs = $this->actions[$action]; + } elseif (isset($this->actions['*'])) { + $verbs = $this->actions['*']; + } else { + return $event->isValid; + } - /** - * @param ActionEvent $event - * @return boolean - * @throws HttpException when the request method is not allowed. - */ - public function beforeAction($event) - { - $action = $event->action->id; - if (isset($this->actions[$action])) { - $verbs = $this->actions[$action]; - } elseif (isset($this->actions['*'])) { - $verbs = $this->actions['*']; - } else { - return $event->isValid; - } + $verb = Yii::$app->getRequest()->getMethod(); + $allowed = array_map('strtoupper', $verbs); + if (!in_array($verb, $allowed)) { + $event->isValid = false; + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 + Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed)); + throw new MethodNotAllowedHttpException('Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed) . '.'); + } - $verb = Yii::$app->getRequest()->getMethod(); - $allowed = array_map('strtoupper', $verbs); - if (!in_array($verb, $allowed)) { - $event->isValid = false; - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 - Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed)); - throw new MethodNotAllowedHttpException('Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed) . '.'); - } - - return $event->isValid; - } + return $event->isValid; + } } diff --git a/framework/web/View.php b/framework/web/View.php index 7c95f5ef242..ab9e51b73b3 100644 --- a/framework/web/View.php +++ b/framework/web/View.php @@ -40,506 +40,508 @@ */ class View extends \yii\base\View { - const EVENT_BEGIN_BODY = 'beginBody'; - /** - * @event Event an event that is triggered by [[endBody()]]. - */ - const EVENT_END_BODY = 'endBody'; - - /** - * The location of registered JavaScript code block or files. - * This means the location is in the head section. - */ - const POS_HEAD = 1; - /** - * The location of registered JavaScript code block or files. - * This means the location is at the beginning of the body section. - */ - const POS_BEGIN = 2; - /** - * The location of registered JavaScript code block or files. - * This means the location is at the end of the body section. - */ - const POS_END = 3; - /** - * The location of registered JavaScript code block. - * This means the JavaScript code block will be enclosed within `jQuery(document).ready()`. - */ - const POS_READY = 4; - /** - * The location of registered JavaScript code block. - * This means the JavaScript code block will be enclosed within `jQuery(window).load()`. - */ - const POS_LOAD = 5; - /** - * This is internally used as the placeholder for receiving the content registered for the head section. - */ - const PH_HEAD = ''; - /** - * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. - */ - const PH_BODY_BEGIN = ''; - /** - * This is internally used as the placeholder for receiving the content registered for the end of the body section. - */ - const PH_BODY_END = ''; - - /** - * @var AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values - * are the registered [[AssetBundle]] objects. - * @see registerAssetBundle() - */ - public $assetBundles = []; - /** - * @var string the page title - */ - public $title; - /** - * @var array the registered meta tags. - * @see registerMetaTag() - */ - public $metaTags; - /** - * @var array the registered link tags. - * @see registerLinkTag() - */ - public $linkTags; - /** - * @var array the registered CSS code blocks. - * @see registerCss() - */ - public $css; - /** - * @var array the registered CSS files. - * @see registerCssFile() - */ - public $cssFiles; - /** - * @var array the registered JS code blocks - * @see registerJs() - */ - public $js; - /** - * @var array the registered JS files. - * @see registerJsFile() - */ - public $jsFiles; - - private $_assetManager; - - - /** - * Marks the position of an HTML head section. - */ - public function head() - { - echo self::PH_HEAD; - } - - /** - * Marks the beginning of an HTML body section. - */ - public function beginBody() - { - echo self::PH_BODY_BEGIN; - $this->trigger(self::EVENT_BEGIN_BODY); - } - - /** - * Marks the ending of an HTML body section. - */ - public function endBody() - { - $this->trigger(self::EVENT_END_BODY); - echo self::PH_BODY_END; - - foreach (array_keys($this->assetBundles) as $bundle) { - $this->registerAssetFiles($bundle); - } - } - - /** - * Marks the ending of an HTML page. - * @param boolean $ajaxMode whether the view is rendering in AJAX mode. - * If true, the JS scripts registered at [[POS_READY]] and [[POS_LOAD]] positions - * will be rendered at the end of the view like normal scripts. - */ - public function endPage($ajaxMode = false) - { - $this->trigger(self::EVENT_END_PAGE); - - $content = ob_get_clean(); - - echo strtr($content, [ - self::PH_HEAD => $this->renderHeadHtml(), - self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(), - self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode), - ]); - - $this->clear(); - } - - /** - * Renders a view in response to an AJAX request. - * - * This method is similar to [[render()]] except that it will surround the view being rendered - * with the calls of [[beginPage()]], [[head()]], [[beginBody()]], [[endBody()]] and [[endPage()]]. - * By doing so, the method is able to inject into the rendering result with JS/CSS scripts and files - * that are registered with the view. - * - * @param string $view the view name. Please refer to [[render()]] on how to specify this parameter. - * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param object $context the context that the view should use for rendering the view. If null, - * existing [[context]] will be used. - * @return string the rendering result - * @see render() - */ - public function renderAjax($view, $params = [], $context = null) - { - $viewFile = $this->findViewFile($view, $context); - - ob_start(); - ob_implicit_flush(false); - - $this->beginPage(); - $this->head(); - $this->beginBody(); - echo $this->renderFile($viewFile, $params, $context); - $this->endBody(); - $this->endPage(true); - - return ob_get_clean(); - } - - /** - * Registers the asset manager being used by this view object. - * @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component. - */ - public function getAssetManager() - { - return $this->_assetManager ?: Yii::$app->getAssetManager(); - } - - /** - * Sets the asset manager. - * @param \yii\web\AssetManager $value the asset manager - */ - public function setAssetManager($value) - { - $this->_assetManager = $value; - } - - /** - * Clears up the registered meta tags, link tags, css/js scripts and files. - */ - public function clear() - { - $this->metaTags = null; - $this->linkTags = null; - $this->css = null; - $this->cssFiles = null; - $this->js = null; - $this->jsFiles = null; - } - - /** - * Registers all files provided by an asset bundle including depending bundles files. - * Removes a bundle from [[assetBundles]] once files are registered. - * @param string $name name of the bundle to register - */ - private function registerAssetFiles($name) - { - if (!isset($this->assetBundles[$name])) { - return; - } - $bundle = $this->assetBundles[$name]; - if ($bundle) { - foreach ($bundle->depends as $dep) { - $this->registerAssetFiles($dep); - } - $bundle->registerAssetFiles($this); - } - unset($this->assetBundles[$name]); - } - - /** - * Registers the named asset bundle. - * All dependent asset bundles will be registered. - * @param string $name the name of the asset bundle. - * @param integer|null $position if set, this forces a minimum position for javascript files. - * This will adjust depending assets javascript file position or fail if requirement can not be met. - * If this is null, asset bundles position settings will not be changed. - * See [[registerJsFile]] for more details on javascript position. - * @return AssetBundle the registered asset bundle instance - * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected - */ - public function registerAssetBundle($name, $position = null) - { - if (!isset($this->assetBundles[$name])) { - $am = $this->getAssetManager(); - $bundle = $am->getBundle($name); - $this->assetBundles[$name] = false; - // register dependencies - $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; - foreach ($bundle->depends as $dep) { - $this->registerAssetBundle($dep, $pos); - } - $this->assetBundles[$name] = $bundle; - } elseif ($this->assetBundles[$name] === false) { - throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); - } else { - $bundle = $this->assetBundles[$name]; - } - - if ($position !== null) { - $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; - if ($pos === null) { - $bundle->jsOptions['position'] = $pos = $position; - } elseif ($pos > $position) { - throw new InvalidConfigException("An asset bundle that depends on '$name' has a higher javascript file position configured than '$name'."); - } - // update position for all dependencies - foreach ($bundle->depends as $dep) { - $this->registerAssetBundle($dep, $pos); - } - } - return $bundle; - } - - /** - * Registers a meta tag. - * @param array $options the HTML attributes for the meta tag. - * @param string $key the key that identifies the meta tag. If two meta tags are registered - * with the same key, the latter will overwrite the former. If this is null, the new meta tag - * will be appended to the existing ones. - */ - public function registerMetaTag($options, $key = null) - { - if ($key === null) { - $this->metaTags[] = Html::tag('meta', '', $options); - } else { - $this->metaTags[$key] = Html::tag('meta', '', $options); - } - } - - /** - * Registers a link tag. - * @param array $options the HTML attributes for the link tag. - * @param string $key the key that identifies the link tag. If two link tags are registered - * with the same key, the latter will overwrite the former. If this is null, the new link tag - * will be appended to the existing ones. - */ - public function registerLinkTag($options, $key = null) - { - if ($key === null) { - $this->linkTags[] = Html::tag('link', '', $options); - } else { - $this->linkTags[$key] = Html::tag('link', '', $options); - } - } - - /** - * Registers a CSS code block. - * @param string $css the CSS code block to be registered - * @param array $options the HTML attributes for the style tag. - * @param string $key the key that identifies the CSS code block. If null, it will use - * $css as the key. If two CSS code blocks are registered with the same key, the latter - * will overwrite the former. - */ - public function registerCss($css, $options = [], $key = null) - { - $key = $key ?: md5($css); - $this->css[$key] = Html::style($css, $options); - } - - /** - * Registers a CSS file. - * @param string $url the CSS file to be registered. - * @param array $depends the names of the asset bundles that this CSS file depends on - * @param array $options the HTML attributes for the link tag. - * @param string $key the key that identifies the CSS script file. If null, it will use - * $url as the key. If two CSS files are registered with the same key, the latter - * will overwrite the former. - */ - public function registerCssFile($url, $depends = [], $options = [], $key = null) - { - $url = Yii::getAlias($url); - $key = $key ?: $url; - if (empty($depends)) { - $this->cssFiles[$key] = Html::cssFile($url, $options); - } else { - $am = Yii::$app->getAssetManager(); - $am->bundles[$key] = new AssetBundle([ - 'css' => [$url], - 'cssOptions' => $options, - 'depends' => (array)$depends, - ]); - $this->registerAssetBundle($key); - } - } - - /** - * Registers a JS code block. - * @param string $js the JS code block to be registered - * @param integer $position the position at which the JS script tag should be inserted - * in a page. The possible values are: - * - * - [[POS_HEAD]]: in the head section - * - [[POS_BEGIN]]: at the beginning of the body section - * - [[POS_END]]: at the end of the body section - * - [[POS_LOAD]]: enclosed within jQuery(window).load(). - * Note that by using this position, the method will automatically register the jQuery js file. - * - [[POS_READY]]: enclosed within jQuery(document).ready(). This is the default value. - * Note that by using this position, the method will automatically register the jQuery js file. - * - * @param string $key the key that identifies the JS code block. If null, it will use - * $js as the key. If two JS code blocks are registered with the same key, the latter - * will overwrite the former. - */ - public function registerJs($js, $position = self::POS_READY, $key = null) - { - $key = $key ?: md5($js); - $this->js[$position][$key] = $js; - if ($position === self::POS_READY || $position === self::POS_LOAD) { - JqueryAsset::register($this); - } - } - - /** - * Registers a JS file. - * @param string $url the JS file to be registered. - * @param array $depends the names of the asset bundles that this JS file depends on - * @param array $options the HTML attributes for the script tag. A special option - * named "position" is supported which specifies where the JS script tag should be inserted - * in a page. The possible values of "position" are: - * - * - [[POS_HEAD]]: in the head section - * - [[POS_BEGIN]]: at the beginning of the body section - * - [[POS_END]]: at the end of the body section. This is the default value. - * - * @param string $key the key that identifies the JS script file. If null, it will use - * $url as the key. If two JS files are registered with the same key, the latter - * will overwrite the former. - */ - public function registerJsFile($url, $depends = [], $options = [], $key = null) - { - $url = Yii::getAlias($url); - $key = $key ?: $url; - if (empty($depends)) { - $position = isset($options['position']) ? $options['position'] : self::POS_END; - unset($options['position']); - $this->jsFiles[$position][$key] = Html::jsFile($url, $options); - } else { - $am = Yii::$app->getAssetManager(); - if (strpos($url, '/') !== 0 && strpos($url, '://') === false) { - $url = Yii::$app->getRequest()->getBaseUrl() . '/' . $url; - } - $am->bundles[$key] = new AssetBundle([ - 'js' => [$url], - 'jsOptions' => $options, - 'depends' => (array)$depends, - ]); - $this->registerAssetBundle($key); - } - } - - /** - * Renders the content to be inserted in the head section. - * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files. - * @return string the rendered content - */ - protected function renderHeadHtml() - { - $lines = []; - if (!empty($this->metaTags)) { - $lines[] = implode("\n", $this->metaTags); - } - - $request = Yii::$app->getRequest(); - if ($request instanceof \yii\web\Request && $request->enableCsrfValidation && !$request->getIsAjax()) { - $lines[] = Html::tag('meta', '', ['name' => 'csrf-param', 'content' => $request->csrfParam]); - $lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]); - } - - if (!empty($this->linkTags)) { - $lines[] = implode("\n", $this->linkTags); - } - if (!empty($this->cssFiles)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->css)) { - $lines[] = implode("\n", $this->css); - } - if (!empty($this->jsFiles[self::POS_HEAD])) { - $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); - } - if (!empty($this->js[self::POS_HEAD])) { - $lines[] = Html::script(implode("\n", $this->js[self::POS_HEAD]), ['type' => 'text/javascript']); - } - return empty($lines) ? '' : implode("\n", $lines); - } - - /** - * Renders the content to be inserted at the beginning of the body section. - * The content is rendered using the registered JS code blocks and files. - * @return string the rendered content - */ - protected function renderBodyBeginHtml() - { - $lines = []; - if (!empty($this->jsFiles[self::POS_BEGIN])) { - $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); - } - if (!empty($this->js[self::POS_BEGIN])) { - $lines[] = Html::script(implode("\n", $this->js[self::POS_BEGIN]), ['type' => 'text/javascript']); - } - return empty($lines) ? '' : implode("\n", $lines); - } - - /** - * Renders the content to be inserted at the end of the body section. - * The content is rendered using the registered JS code blocks and files. - * @param boolean $ajaxMode whether the view is rendering in AJAX mode. - * If true, the JS scripts registered at [[POS_READY]] and [[POS_LOAD]] positions - * will be rendered at the end of the view like normal scripts. - * @return string the rendered content - */ - protected function renderBodyEndHtml($ajaxMode) - { - $lines = []; - - if (!empty($this->jsFiles[self::POS_END])) { - $lines[] = implode("\n", $this->jsFiles[self::POS_END]); - } - - if ($ajaxMode) { - $scripts = []; - if (!empty($this->js[self::POS_END])) { - $scripts[] = implode("\n", $this->js[self::POS_END]); - } - if (!empty($this->js[self::POS_READY])) { - $scripts[] = implode("\n", $this->js[self::POS_READY]); - } - if (!empty($this->js[self::POS_LOAD])) { - $scripts[] = implode("\n", $this->js[self::POS_LOAD]); - } - if (!empty($scripts)) { - $lines[] = Html::script(implode("\n", $scripts), ['type' => 'text/javascript']); - } - } else { - if (!empty($this->js[self::POS_END])) { - $lines[] = Html::script(implode("\n", $this->js[self::POS_END]), ['type' => 'text/javascript']); - } - if (!empty($this->js[self::POS_READY])) { - $js = "jQuery(document).ready(function(){\n" . implode("\n", $this->js[self::POS_READY]) . "\n});"; - $lines[] = Html::script($js, ['type' => 'text/javascript']); - } - if (!empty($this->js[self::POS_LOAD])) { - $js = "jQuery(window).load(function(){\n" . implode("\n", $this->js[self::POS_LOAD]) . "\n});"; - $lines[] = Html::script($js, ['type' => 'text/javascript']); - } - } - - return empty($lines) ? '' : implode("\n", $lines); - } + const EVENT_BEGIN_BODY = 'beginBody'; + /** + * @event Event an event that is triggered by [[endBody()]]. + */ + const EVENT_END_BODY = 'endBody'; + + /** + * The location of registered JavaScript code block or files. + * This means the location is in the head section. + */ + const POS_HEAD = 1; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the beginning of the body section. + */ + const POS_BEGIN = 2; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the end of the body section. + */ + const POS_END = 3; + /** + * The location of registered JavaScript code block. + * This means the JavaScript code block will be enclosed within `jQuery(document).ready()`. + */ + const POS_READY = 4; + /** + * The location of registered JavaScript code block. + * This means the JavaScript code block will be enclosed within `jQuery(window).load()`. + */ + const POS_LOAD = 5; + /** + * This is internally used as the placeholder for receiving the content registered for the head section. + */ + const PH_HEAD = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. + */ + const PH_BODY_BEGIN = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the end of the body section. + */ + const PH_BODY_END = ''; + + /** + * @var AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values + * are the registered [[AssetBundle]] objects. + * @see registerAssetBundle() + */ + public $assetBundles = []; + /** + * @var string the page title + */ + public $title; + /** + * @var array the registered meta tags. + * @see registerMetaTag() + */ + public $metaTags; + /** + * @var array the registered link tags. + * @see registerLinkTag() + */ + public $linkTags; + /** + * @var array the registered CSS code blocks. + * @see registerCss() + */ + public $css; + /** + * @var array the registered CSS files. + * @see registerCssFile() + */ + public $cssFiles; + /** + * @var array the registered JS code blocks + * @see registerJs() + */ + public $js; + /** + * @var array the registered JS files. + * @see registerJsFile() + */ + public $jsFiles; + + private $_assetManager; + + /** + * Marks the position of an HTML head section. + */ + public function head() + { + echo self::PH_HEAD; + } + + /** + * Marks the beginning of an HTML body section. + */ + public function beginBody() + { + echo self::PH_BODY_BEGIN; + $this->trigger(self::EVENT_BEGIN_BODY); + } + + /** + * Marks the ending of an HTML body section. + */ + public function endBody() + { + $this->trigger(self::EVENT_END_BODY); + echo self::PH_BODY_END; + + foreach (array_keys($this->assetBundles) as $bundle) { + $this->registerAssetFiles($bundle); + } + } + + /** + * Marks the ending of an HTML page. + * @param boolean $ajaxMode whether the view is rendering in AJAX mode. + * If true, the JS scripts registered at [[POS_READY]] and [[POS_LOAD]] positions + * will be rendered at the end of the view like normal scripts. + */ + public function endPage($ajaxMode = false) + { + $this->trigger(self::EVENT_END_PAGE); + + $content = ob_get_clean(); + + echo strtr($content, [ + self::PH_HEAD => $this->renderHeadHtml(), + self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode), + ]); + + $this->clear(); + } + + /** + * Renders a view in response to an AJAX request. + * + * This method is similar to [[render()]] except that it will surround the view being rendered + * with the calls of [[beginPage()]], [[head()]], [[beginBody()]], [[endBody()]] and [[endPage()]]. + * By doing so, the method is able to inject into the rendering result with JS/CSS scripts and files + * that are registered with the view. + * + * @param string $view the view name. Please refer to [[render()]] on how to specify this parameter. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. + * @return string the rendering result + * @see render() + */ + public function renderAjax($view, $params = [], $context = null) + { + $viewFile = $this->findViewFile($view, $context); + + ob_start(); + ob_implicit_flush(false); + + $this->beginPage(); + $this->head(); + $this->beginBody(); + echo $this->renderFile($viewFile, $params, $context); + $this->endBody(); + $this->endPage(true); + + return ob_get_clean(); + } + + /** + * Registers the asset manager being used by this view object. + * @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component. + */ + public function getAssetManager() + { + return $this->_assetManager ?: Yii::$app->getAssetManager(); + } + + /** + * Sets the asset manager. + * @param \yii\web\AssetManager $value the asset manager + */ + public function setAssetManager($value) + { + $this->_assetManager = $value; + } + + /** + * Clears up the registered meta tags, link tags, css/js scripts and files. + */ + public function clear() + { + $this->metaTags = null; + $this->linkTags = null; + $this->css = null; + $this->cssFiles = null; + $this->js = null; + $this->jsFiles = null; + } + + /** + * Registers all files provided by an asset bundle including depending bundles files. + * Removes a bundle from [[assetBundles]] once files are registered. + * @param string $name name of the bundle to register + */ + private function registerAssetFiles($name) + { + if (!isset($this->assetBundles[$name])) { + return; + } + $bundle = $this->assetBundles[$name]; + if ($bundle) { + foreach ($bundle->depends as $dep) { + $this->registerAssetFiles($dep); + } + $bundle->registerAssetFiles($this); + } + unset($this->assetBundles[$name]); + } + + /** + * Registers the named asset bundle. + * All dependent asset bundles will be registered. + * @param string $name the name of the asset bundle. + * @param integer|null $position if set, this forces a minimum position for javascript files. + * This will adjust depending assets javascript file position or fail if requirement can not be met. + * If this is null, asset bundles position settings will not be changed. + * See [[registerJsFile]] for more details on javascript position. + * @return AssetBundle the registered asset bundle instance + * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected + */ + public function registerAssetBundle($name, $position = null) + { + if (!isset($this->assetBundles[$name])) { + $am = $this->getAssetManager(); + $bundle = $am->getBundle($name); + $this->assetBundles[$name] = false; + // register dependencies + $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; + foreach ($bundle->depends as $dep) { + $this->registerAssetBundle($dep, $pos); + } + $this->assetBundles[$name] = $bundle; + } elseif ($this->assetBundles[$name] === false) { + throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); + } else { + $bundle = $this->assetBundles[$name]; + } + + if ($position !== null) { + $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null; + if ($pos === null) { + $bundle->jsOptions['position'] = $pos = $position; + } elseif ($pos > $position) { + throw new InvalidConfigException("An asset bundle that depends on '$name' has a higher javascript file position configured than '$name'."); + } + // update position for all dependencies + foreach ($bundle->depends as $dep) { + $this->registerAssetBundle($dep, $pos); + } + } + + return $bundle; + } + + /** + * Registers a meta tag. + * @param array $options the HTML attributes for the meta tag. + * @param string $key the key that identifies the meta tag. If two meta tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new meta tag + * will be appended to the existing ones. + */ + public function registerMetaTag($options, $key = null) + { + if ($key === null) { + $this->metaTags[] = Html::tag('meta', '', $options); + } else { + $this->metaTags[$key] = Html::tag('meta', '', $options); + } + } + + /** + * Registers a link tag. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the link tag. If two link tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new link tag + * will be appended to the existing ones. + */ + public function registerLinkTag($options, $key = null) + { + if ($key === null) { + $this->linkTags[] = Html::tag('link', '', $options); + } else { + $this->linkTags[$key] = Html::tag('link', '', $options); + } + } + + /** + * Registers a CSS code block. + * @param string $css the CSS code block to be registered + * @param array $options the HTML attributes for the style tag. + * @param string $key the key that identifies the CSS code block. If null, it will use + * $css as the key. If two CSS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCss($css, $options = [], $key = null) + { + $key = $key ?: md5($css); + $this->css[$key] = Html::style($css, $options); + } + + /** + * Registers a CSS file. + * @param string $url the CSS file to be registered. + * @param array $depends the names of the asset bundles that this CSS file depends on + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the CSS script file. If null, it will use + * $url as the key. If two CSS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCssFile($url, $depends = [], $options = [], $key = null) + { + $url = Yii::getAlias($url); + $key = $key ?: $url; + if (empty($depends)) { + $this->cssFiles[$key] = Html::cssFile($url, $options); + } else { + $am = Yii::$app->getAssetManager(); + $am->bundles[$key] = new AssetBundle([ + 'css' => [$url], + 'cssOptions' => $options, + 'depends' => (array) $depends, + ]); + $this->registerAssetBundle($key); + } + } + + /** + * Registers a JS code block. + * @param string $js the JS code block to be registered + * @param integer $position the position at which the JS script tag should be inserted + * in a page. The possible values are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * - [[POS_LOAD]]: enclosed within jQuery(window).load(). + * Note that by using this position, the method will automatically register the jQuery js file. + * - [[POS_READY]]: enclosed within jQuery(document).ready(). This is the default value. + * Note that by using this position, the method will automatically register the jQuery js file. + * + * @param string $key the key that identifies the JS code block. If null, it will use + * $js as the key. If two JS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJs($js, $position = self::POS_READY, $key = null) + { + $key = $key ?: md5($js); + $this->js[$position][$key] = $js; + if ($position === self::POS_READY || $position === self::POS_LOAD) { + JqueryAsset::register($this); + } + } + + /** + * Registers a JS file. + * @param string $url the JS file to be registered. + * @param array $depends the names of the asset bundles that this JS file depends on + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section. This is the default value. + * + * @param string $key the key that identifies the JS script file. If null, it will use + * $url as the key. If two JS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJsFile($url, $depends = [], $options = [], $key = null) + { + $url = Yii::getAlias($url); + $key = $key ?: $url; + if (empty($depends)) { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $this->jsFiles[$position][$key] = Html::jsFile($url, $options); + } else { + $am = Yii::$app->getAssetManager(); + if (strpos($url, '/') !== 0 && strpos($url, '://') === false) { + $url = Yii::$app->getRequest()->getBaseUrl() . '/' . $url; + } + $am->bundles[$key] = new AssetBundle([ + 'js' => [$url], + 'jsOptions' => $options, + 'depends' => (array) $depends, + ]); + $this->registerAssetBundle($key); + } + } + + /** + * Renders the content to be inserted in the head section. + * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files. + * @return string the rendered content + */ + protected function renderHeadHtml() + { + $lines = []; + if (!empty($this->metaTags)) { + $lines[] = implode("\n", $this->metaTags); + } + + $request = Yii::$app->getRequest(); + if ($request instanceof \yii\web\Request && $request->enableCsrfValidation && !$request->getIsAjax()) { + $lines[] = Html::tag('meta', '', ['name' => 'csrf-param', 'content' => $request->csrfParam]); + $lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]); + } + + if (!empty($this->linkTags)) { + $lines[] = implode("\n", $this->linkTags); + } + if (!empty($this->cssFiles)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->css)) { + $lines[] = implode("\n", $this->css); + } + if (!empty($this->jsFiles[self::POS_HEAD])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); + } + if (!empty($this->js[self::POS_HEAD])) { + $lines[] = Html::script(implode("\n", $this->js[self::POS_HEAD]), ['type' => 'text/javascript']); + } + + return empty($lines) ? '' : implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the beginning of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyBeginHtml() + { + $lines = []; + if (!empty($this->jsFiles[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); + } + if (!empty($this->js[self::POS_BEGIN])) { + $lines[] = Html::script(implode("\n", $this->js[self::POS_BEGIN]), ['type' => 'text/javascript']); + } + + return empty($lines) ? '' : implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the end of the body section. + * The content is rendered using the registered JS code blocks and files. + * @param boolean $ajaxMode whether the view is rendering in AJAX mode. + * If true, the JS scripts registered at [[POS_READY]] and [[POS_LOAD]] positions + * will be rendered at the end of the view like normal scripts. + * @return string the rendered content + */ + protected function renderBodyEndHtml($ajaxMode) + { + $lines = []; + + if (!empty($this->jsFiles[self::POS_END])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_END]); + } + + if ($ajaxMode) { + $scripts = []; + if (!empty($this->js[self::POS_END])) { + $scripts[] = implode("\n", $this->js[self::POS_END]); + } + if (!empty($this->js[self::POS_READY])) { + $scripts[] = implode("\n", $this->js[self::POS_READY]); + } + if (!empty($this->js[self::POS_LOAD])) { + $scripts[] = implode("\n", $this->js[self::POS_LOAD]); + } + if (!empty($scripts)) { + $lines[] = Html::script(implode("\n", $scripts), ['type' => 'text/javascript']); + } + } else { + if (!empty($this->js[self::POS_END])) { + $lines[] = Html::script(implode("\n", $this->js[self::POS_END]), ['type' => 'text/javascript']); + } + if (!empty($this->js[self::POS_READY])) { + $js = "jQuery(document).ready(function () {\n" . implode("\n", $this->js[self::POS_READY]) . "\n});"; + $lines[] = Html::script($js, ['type' => 'text/javascript']); + } + if (!empty($this->js[self::POS_LOAD])) { + $js = "jQuery(window).load(function () {\n" . implode("\n", $this->js[self::POS_LOAD]) . "\n});"; + $lines[] = Html::script($js, ['type' => 'text/javascript']); + } + } + + return empty($lines) ? '' : implode("\n", $lines); + } } diff --git a/framework/web/XmlResponseFormatter.php b/framework/web/XmlResponseFormatter.php index 292424a94cc..3860c914161 100644 --- a/framework/web/XmlResponseFormatter.php +++ b/framework/web/XmlResponseFormatter.php @@ -24,75 +24,75 @@ */ class XmlResponseFormatter extends Component implements ResponseFormatterInterface { - /** - * @var string the Content-Type header for the response - */ - public $contentType = 'application/xml'; - /** - * @var string the XML version - */ - public $version = '1.0'; - /** - * @var string the XML encoding. If not set, it will use the value of [[Response::charset]]. - */ - public $encoding; - /** - * @var string the name of the root element. - */ - public $rootTag = 'response'; - /** - * @var string the name of the elements that represent the array elements with numeric keys. - */ - public $itemTag = 'item'; + /** + * @var string the Content-Type header for the response + */ + public $contentType = 'application/xml'; + /** + * @var string the XML version + */ + public $version = '1.0'; + /** + * @var string the XML encoding. If not set, it will use the value of [[Response::charset]]. + */ + public $encoding; + /** + * @var string the name of the root element. + */ + public $rootTag = 'response'; + /** + * @var string the name of the elements that represent the array elements with numeric keys. + */ + public $itemTag = 'item'; - /** - * Formats the specified response. - * @param Response $response the response to be formatted. - */ - public function format($response) - { - $response->getHeaders()->set('Content-Type', $this->contentType); - $dom = new DOMDocument($this->version, $this->encoding === null ? $response->charset : $this->encoding); - $root = new DOMElement($this->rootTag); - $dom->appendChild($root); - $this->buildXml($root, $response->data); - $response->content = $dom->saveXML(); - } + /** + * Formats the specified response. + * @param Response $response the response to be formatted. + */ + public function format($response) + { + $response->getHeaders()->set('Content-Type', $this->contentType); + $dom = new DOMDocument($this->version, $this->encoding === null ? $response->charset : $this->encoding); + $root = new DOMElement($this->rootTag); + $dom->appendChild($root); + $this->buildXml($root, $response->data); + $response->content = $dom->saveXML(); + } - /** - * @param DOMElement $element - * @param mixed $data - */ - protected function buildXml($element, $data) - { - if (is_object($data)) { - $child = new DOMElement(StringHelper::basename(get_class($data))); - $element->appendChild($child); - if ($data instanceof Arrayable) { - $this->buildXml($child, $data->toArray()); - } else { - $array = []; - foreach ($data as $name => $value) { - $array[$name] = $value; - } - $this->buildXml($child, $array); - } - } elseif (is_array($data)) { - foreach ($data as $name => $value) { - if (is_int($name) && is_object($value)) { - $this->buildXml($element, $value); - } elseif (is_array($value) || is_object($value)) { - $child = new DOMElement(is_int($name) ? $this->itemTag : $name); - $element->appendChild($child); - $this->buildXml($child, $value); - } else { - $child = new DOMElement(is_int($name) ? $this->itemTag : $name); - $element->appendChild($child); - $child->appendChild(new DOMText((string)$value)); - } - } - } else { - $element->appendChild(new DOMText((string)$data)); - } - } + /** + * @param DOMElement $element + * @param mixed $data + */ + protected function buildXml($element, $data) + { + if (is_object($data)) { + $child = new DOMElement(StringHelper::basename(get_class($data))); + $element->appendChild($child); + if ($data instanceof Arrayable) { + $this->buildXml($child, $data->toArray()); + } else { + $array = []; + foreach ($data as $name => $value) { + $array[$name] = $value; + } + $this->buildXml($child, $array); + } + } elseif (is_array($data)) { + foreach ($data as $name => $value) { + if (is_int($name) && is_object($value)) { + $this->buildXml($element, $value); + } elseif (is_array($value) || is_object($value)) { + $child = new DOMElement(is_int($name) ? $this->itemTag : $name); + $element->appendChild($child); + $this->buildXml($child, $value); + } else { + $child = new DOMElement(is_int($name) ? $this->itemTag : $name); + $element->appendChild($child); + $child->appendChild(new DOMText((string) $value)); + } + } + } else { + $element->appendChild(new DOMText((string) $data)); + } + } } diff --git a/framework/web/YiiAsset.php b/framework/web/YiiAsset.php index d38b7113ade..db7e85ec206 100644 --- a/framework/web/YiiAsset.php +++ b/framework/web/YiiAsset.php @@ -15,11 +15,11 @@ */ class YiiAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'yii.js', - ]; - public $depends = [ - 'yii\web\JqueryAsset', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'yii.js', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + ]; } diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index c93b30b7ddf..92c09ae1dda 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -21,649 +21,667 @@ */ class ActiveField extends Component { - /** - * @var ActiveForm the form that this field is associated with. - */ - public $form; - /** - * @var Model the data model that this field is associated with - */ - public $model; - /** - * @var string the model attribute that this field is associated with - */ - public $attribute; - /** - * @var array the HTML attributes (name-value pairs) for the field container tag. - * The values will be HTML-encoded using [[Html::encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * The following special options are recognized: - * - * - tag: the tag name of the container element. Defaults to "div". - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'form-group']; - /** - * @var string the template that is used to arrange the label, the input field, the error message and the hint text. - * The following tokens will be replaced when [[render()]] is called: `{label}`, `{input}`, `{error}` and `{hint}`. - */ - public $template = "{label}\n{input}\n{hint}\n{error}"; - /** - * @var array the default options for the input tags. The parameter passed to individual input methods - * (e.g. [[textInput()]]) will be merged with this property when rendering the input tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $inputOptions = ['class' => 'form-control']; - /** - * @var array the default options for the error tags. The parameter passed to [[error()]] will be - * merged with this property when rendering the error tag. - * The following special options are recognized: - * - * - tag: the tag name of the container element. Defaults to "div". - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $errorOptions = ['class' => 'help-block']; - /** - * @var array the default options for the label tags. The parameter passed to [[label()]] will be - * merged with this property when rendering the label tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $labelOptions = ['class' => 'control-label']; - /** - * @var array the default options for the hint tags. The parameter passed to [[hint()]] will be - * merged with this property when rendering the hint tag. - * The following special options are recognized: - * - * - tag: the tag name of the container element. Defaults to "div". - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $hintOptions = ['class' => 'hint-block']; - /** - * @var boolean whether to enable client-side data validation. - * If not set, it will take the value of [[ActiveForm::enableClientValidation]]. - */ - public $enableClientValidation; - /** - * @var boolean whether to enable AJAX-based data validation. - * If not set, it will take the value of [[ActiveForm::enableAjaxValidation]]. - */ - public $enableAjaxValidation; - /** - * @var boolean whether to perform validation when the input field loses focus and its value is found changed. - * If not set, it will take the value of [[ActiveForm::validateOnChange]]. - */ - public $validateOnChange; - /** - * @var boolean whether to perform validation while the user is typing in the input field. - * If not set, it will take the value of [[ActiveForm::validateOnType]]. - * @see validationDelay - */ - public $validateOnType; - /** - * @var integer number of milliseconds that the validation should be delayed when the input field - * is changed or the user types in the field. - * If not set, it will take the value of [[ActiveForm::validationDelay]]. - */ - public $validationDelay; - /** - * @var array the jQuery selectors for selecting the container, input and error tags. - * The array keys should be "container", "input", and/or "error", and the array values - * are the corresponding selectors. For example, `['input' => '#my-input']`. - * - * The container selector is used under the context of the form, while the input and the error - * selectors are used under the context of the container. - * - * You normally do not need to set this property as the default selectors should work well for most cases. - */ - public $selectors; - /** - * @var array different parts of the field (e.g. input, label). This will be used together with - * [[template]] to generate the final field HTML code. The keys are the token names in [[template]], - * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}` and `{error}`. - * Note that you normally don't need to access this property directly as - * it is maintained by various methods of this class. - */ - public $parts = []; - - - /** - * PHP magic method that returns the string representation of this object. - * @return string the string representation of this object. - */ - public function __toString() - { - // __toString cannot throw exception - // use trigger_error to bypass this limitation - try { - return $this->render(); - } catch (\Exception $e) { - trigger_error($e->getMessage() . "\n\n" . $e->getTraceAsString()); - return ''; - } - } - - /** - * Renders the whole field. - * This method will generate the label, error tag, input tag and hint tag (if any), and - * assemble them into HTML according to [[template]]. - * @param string|callable $content the content within the field container. - * If null (not set), the default methods will be called to generate the label, error tag and input tag, - * and use them as the content. - * If a callable, it will be called to generate the content. The signature of the callable should be: - * - * ~~~ - * function ($field) { - * return $html; - * } - * ~~~ - * - * @return string the rendering result - */ - public function render($content = null) - { - if ($content === null) { - if (!isset($this->parts['{input}'])) { - $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $this->inputOptions); - } - if (!isset($this->parts['{label}'])) { - $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $this->labelOptions); - } - if (!isset($this->parts['{error}'])) { - $this->parts['{error}'] = Html::error($this->model, $this->attribute, $this->errorOptions); - } - if (!isset($this->parts['{hint}'])) { - $this->parts['{hint}'] = ''; - } - $content = strtr($this->template, $this->parts); - } elseif (!is_string($content)) { - $content = call_user_func($content, $this); - } - return $this->begin() . "\n" . $content . "\n" . $this->end(); - } - - /** - * Renders the opening tag of the field container. - * @return string the rendering result. - */ - public function begin() - { - $clientOptions = $this->getClientOptions(); - if (!empty($clientOptions)) { - $this->form->attributes[$this->attribute] = $clientOptions; - } - - $inputID = Html::getInputId($this->model, $this->attribute); - $attribute = Html::getAttributeName($this->attribute); - $options = $this->options; - $class = isset($options['class']) ? [$options['class']] : []; - $class[] = "field-$inputID"; - if ($this->model->isAttributeRequired($attribute)) { - $class[] = $this->form->requiredCssClass; - } - if ($this->model->hasErrors($attribute)) { - $class[] = $this->form->errorCssClass; - } - $options['class'] = implode(' ', $class); - $tag = ArrayHelper::remove($options, 'tag', 'div'); - - return Html::beginTag($tag, $options); - } - - /** - * Renders the closing tag of the field container. - * @return string the rendering result. - */ - public function end() - { - return Html::endTag(isset($this->options['tag']) ? $this->options['tag'] : 'div'); - } - - /** - * Generates a label tag for [[attribute]]. - * @param string $label the label to use. If null, it will be generated via [[Model::getAttributeLabel()]]. - * Note that this will NOT be [[Html::encode()|encoded]]. - * @param array $options the tag options in terms of name-value pairs. It will be merged with [[labelOptions]]. - * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded - * using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * @return static the field object itself - */ - public function label($label = null, $options = []) - { - $options = array_merge($this->labelOptions, $options); - if ($label !== null) { - $options['label'] = $label; - } - $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $options); - return $this; - } - - /** - * Generates a tag that contains the first validation error of [[attribute]]. - * Note that even if there is no validation error, this method will still return an empty error tag. - * @param array $options the tag options in terms of name-value pairs. It will be merged with [[errorOptions]]. - * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded - * using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * The following options are specially handled: - * - * - tag: this specifies the tag name. If not set, "div" will be used. - * - * @return static the field object itself - */ - public function error($options = []) - { - $options = array_merge($this->errorOptions, $options); - $this->parts['{error}'] = Html::error($this->model, $this->attribute, $options); - return $this; - } - - /** - * Renders the hint tag. - * @param string $content the hint content. It will NOT be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the hint tag. The values will be HTML-encoded using [[Html::encode()]]. - * - * The following options are specially handled: - * - * - tag: this specifies the tag name. If not set, "div" will be used. - * - * @return static the field object itself - */ - public function hint($content, $options = []) - { - $options = array_merge($this->hintOptions, $options); - $tag = ArrayHelper::remove($options, 'tag', 'div'); - $this->parts['{hint}'] = Html::tag($tag, $content, $options); - return $this; - } - - /** - * Renders an input tag. - * @param string $type the input type (e.g. 'text', 'password') - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. - * @return static the field object itself - */ - public function input($type, $options = []) - { - $options = array_merge($this->inputOptions, $options); - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options); - return $this; - } - - /** - * Renders a text input. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. - * @return static the field object itself - */ - public function textInput($options = []) - { - $options = array_merge($this->inputOptions, $options); - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options); - return $this; - } - - /** - * Renders a password input. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. - * @return static the field object itself - */ - public function passwordInput($options = []) - { - $options = array_merge($this->inputOptions, $options); - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options); - return $this; - } - - /** - * Renders a file input. - * This method will generate the "name" and "value" tag attributes automatically for the model attribute - * unless they are explicitly specified in `$options`. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. - * @return static the field object itself - */ - public function fileInput($options = []) - { - // https://github.com/yiisoft/yii2/pull/795 - if ($this->inputOptions !== ['class' => 'form-control']) { - $options = array_merge($this->inputOptions, $options); - } - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options); - return $this; - } - - /** - * Renders a text area. - * The model attribute value will be used as the content in the textarea. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. - * @return static the field object itself - */ - public function textarea($options = []) - { - $options = array_merge($this->inputOptions, $options); - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options); - return $this; - } - - /** - * Renders a radio button. - * This method will generate the "checked" tag attribute according to the model attribute value. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, - * it will take the default value '0'. This method will render a hidden input so that if the radio button - * is not checked and is submitted, the value of this attribute will still be submitted to the server - * via the hidden input. - * - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass - * in HTML code such as an image tag. If this is is coming from end users, you should [[Html::encode()]] it to prevent XSS attacks. - * When this option is specified, the radio button will be enclosed by a label tag. - * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * @param boolean $enclosedByLabel whether to enclose the radio within the label. - * If true, the method will still use [[template]] to layout the checkbox and the error message - * except that the radio is enclosed by the label tag. - * @return static the field object itself - */ - public function radio($options = [], $enclosedByLabel = true) - { - if ($enclosedByLabel) { - if (!isset($options['label'])) { - $attribute = Html::getAttributeName($this->attribute); - $options['label'] = Html::encode($this->model->getAttributeLabel($attribute)); - } - $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); - $this->parts['{label}'] = ''; - } else { - $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); - } - $this->adjustLabelFor($options); - return $this; - } - - /** - * Renders a checkbox. - * This method will generate the "checked" tag attribute according to the model attribute value. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, - * it will take the default value '0'. This method will render a hidden input so that if the radio button - * is not checked and is submitted, the value of this attribute will still be submitted to the server - * via the hidden input. - * - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass - * in HTML code such as an image tag. If this is is coming from end users, you should [[Html::encode()]] it to prevent XSS attacks. - * When this option is specified, the checkbox will be enclosed by a label tag. - * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * @param boolean $enclosedByLabel whether to enclose the checkbox within the label. - * If true, the method will still use [[template]] to layout the checkbox and the error message - * except that the checkbox is enclosed by the label tag. - * @return static the field object itself - */ - public function checkbox($options = [], $enclosedByLabel = true) - { - if ($enclosedByLabel) { - if (!isset($options['label'])) { - $attribute = Html::getAttributeName($this->attribute); - $options['label'] = Html::encode($this->model->getAttributeLabel($attribute)); - } - $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); - $this->parts['{label}'] = ''; - } else { - $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); - } - $this->adjustLabelFor($options); - return $this; - } - - /** - * Renders a drop-down list. - * The selection of the drop-down list is taken from the value of the model attribute. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return static the field object itself - */ - public function dropDownList($items, $options = []) - { - $options = array_merge($this->inputOptions, $options); - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options); - return $this; - } - - /** - * Renders a list box. - * The selection of the list box is taken from the value of the model attribute. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * [ - * 'value1' => ['disabled' => true], - * 'value2' => ['label' => 'value 2'], - * ]; - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return static the field object itself - */ - public function listBox($items, $options = []) - { - $options = array_merge($this->inputOptions, $options); - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options); - return $this; - } - - /** - * Renders a list of checkboxes. - * A checkbox list allows multiple selection, like [[listBox()]]. - * As a result, the corresponding submitted value is an array. - * The selection of the checkbox list is taken from the value of the model attribute. - * @param array $items the data item used to generate the checkboxes. - * The array values are the labels, while the array keys are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are specially handled: - * - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input. - * @return static the field object itself - */ - public function checkboxList($items, $options = []) - { - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options); - return $this; - } - - /** - * Renders a list of radio buttons. - * A radio button list is like a checkbox list, except that it only allows single selection. - * The selection of the radio buttons is taken from the value of the model attribute. - * @param array $items the data item used to generate the radio buttons. - * The array keys are the labels, while the array values are the corresponding radio button values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are specially handled: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input. - * @return static the field object itself - */ - public function radioList($items, $options = []) - { - $this->adjustLabelFor($options); - $this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options); - return $this; - } - - /** - * Renders a widget as the input of the field. - * - * Note that the widget must have both `model` and `attribute` properties. They will - * be initialized with [[model]] and [[attribute]] of this field, respectively. - * - * If you want to use a widget that does not have `model` and `attribute` properties, - * please use [[render()]] instead. - * - * @param string $class the widget class name - * @param array $config name-value pairs that will be used to initialize the widget - * @return static the field object itself - */ - public function widget($class, $config = []) - { - /** @var \yii\base\Widget $class */ - $config['model'] = $this->model; - $config['attribute'] = $this->attribute; - $config['view'] = $this->form->getView(); - $this->parts['{input}'] = $class::widget($config); - return $this; - } - - /** - * Adjusts the "for" attribute for the label based on the input options. - * @param array $options the input options - */ - protected function adjustLabelFor($options) - { - if (isset($options['id']) && !isset($this->labelOptions['for'])) { - $this->labelOptions['for'] = $options['id']; - } - } - - /** - * Returns the JS options for the field. - * @return array the JS options - */ - protected function getClientOptions() - { - $attribute = Html::getAttributeName($this->attribute); - if (!in_array($attribute, $this->model->activeAttributes(), true)) { - return []; - } - - $options = []; - - $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; - if ($enableClientValidation) { - $validators = []; - foreach ($this->model->getActiveValidators($attribute) as $validator) { - /** @var \yii\validators\Validator $validator */ - $js = $validator->clientValidateAttribute($this->model, $attribute, $this->form->getView()); - if ($validator->enableClientValidation && $js != '') { - $validators[] = $js; - } - } - if (!empty($validators)) { - $options['validate'] = new JsExpression("function(attribute, value, messages) {" . implode('', $validators) . '}'); - } - } - - $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; - if ($enableAjaxValidation) { - $options['enableAjaxValidation'] = 1; - } - - if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { - $inputID = Html::getInputId($this->model, $this->attribute); - $options['name'] = $inputID; - foreach (['validateOnChange', 'validateOnType', 'validationDelay'] as $name) { - $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; - } - $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; - $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; - if (isset($this->errorOptions['class'])) { - $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); - } else { - $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span'; - } - return $options; - } else { - return []; - } - } + /** + * @var ActiveForm the form that this field is associated with. + */ + public $form; + /** + * @var Model the data model that this field is associated with + */ + public $model; + /** + * @var string the model attribute that this field is associated with + */ + public $attribute; + /** + * @var array the HTML attributes (name-value pairs) for the field container tag. + * The values will be HTML-encoded using [[Html::encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * The following special options are recognized: + * + * - tag: the tag name of the container element. Defaults to "div". + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'form-group']; + /** + * @var string the template that is used to arrange the label, the input field, the error message and the hint text. + * The following tokens will be replaced when [[render()]] is called: `{label}`, `{input}`, `{error}` and `{hint}`. + */ + public $template = "{label}\n{input}\n{hint}\n{error}"; + /** + * @var array the default options for the input tags. The parameter passed to individual input methods + * (e.g. [[textInput()]]) will be merged with this property when rendering the input tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $inputOptions = ['class' => 'form-control']; + /** + * @var array the default options for the error tags. The parameter passed to [[error()]] will be + * merged with this property when rendering the error tag. + * The following special options are recognized: + * + * - tag: the tag name of the container element. Defaults to "div". + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $errorOptions = ['class' => 'help-block']; + /** + * @var array the default options for the label tags. The parameter passed to [[label()]] will be + * merged with this property when rendering the label tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $labelOptions = ['class' => 'control-label']; + /** + * @var array the default options for the hint tags. The parameter passed to [[hint()]] will be + * merged with this property when rendering the hint tag. + * The following special options are recognized: + * + * - tag: the tag name of the container element. Defaults to "div". + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $hintOptions = ['class' => 'hint-block']; + /** + * @var boolean whether to enable client-side data validation. + * If not set, it will take the value of [[ActiveForm::enableClientValidation]]. + */ + public $enableClientValidation; + /** + * @var boolean whether to enable AJAX-based data validation. + * If not set, it will take the value of [[ActiveForm::enableAjaxValidation]]. + */ + public $enableAjaxValidation; + /** + * @var boolean whether to perform validation when the input field loses focus and its value is found changed. + * If not set, it will take the value of [[ActiveForm::validateOnChange]]. + */ + public $validateOnChange; + /** + * @var boolean whether to perform validation while the user is typing in the input field. + * If not set, it will take the value of [[ActiveForm::validateOnType]]. + * @see validationDelay + */ + public $validateOnType; + /** + * @var integer number of milliseconds that the validation should be delayed when the input field + * is changed or the user types in the field. + * If not set, it will take the value of [[ActiveForm::validationDelay]]. + */ + public $validationDelay; + /** + * @var array the jQuery selectors for selecting the container, input and error tags. + * The array keys should be "container", "input", and/or "error", and the array values + * are the corresponding selectors. For example, `['input' => '#my-input']`. + * + * The container selector is used under the context of the form, while the input and the error + * selectors are used under the context of the container. + * + * You normally do not need to set this property as the default selectors should work well for most cases. + */ + public $selectors; + /** + * @var array different parts of the field (e.g. input, label). This will be used together with + * [[template]] to generate the final field HTML code. The keys are the token names in [[template]], + * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}` and `{error}`. + * Note that you normally don't need to access this property directly as + * it is maintained by various methods of this class. + */ + public $parts = []; + + + /** + * PHP magic method that returns the string representation of this object. + * @return string the string representation of this object. + */ + public function __toString() + { + // __toString cannot throw exception + // use trigger_error to bypass this limitation + try { + return $this->render(); + } catch (\Exception $e) { + trigger_error($e->getMessage() . "\n\n" . $e->getTraceAsString()); + + return ''; + } + } + + /** + * Renders the whole field. + * This method will generate the label, error tag, input tag and hint tag (if any), and + * assemble them into HTML according to [[template]]. + * @param string|callable $content the content within the field container. + * If null (not set), the default methods will be called to generate the label, error tag and input tag, + * and use them as the content. + * If a callable, it will be called to generate the content. The signature of the callable should be: + * + * ~~~ + * function ($field) { + * return $html; + * } + * ~~~ + * + * @return string the rendering result + */ + public function render($content = null) + { + if ($content === null) { + if (!isset($this->parts['{input}'])) { + $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $this->inputOptions); + } + if (!isset($this->parts['{label}'])) { + $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $this->labelOptions); + } + if (!isset($this->parts['{error}'])) { + $this->parts['{error}'] = Html::error($this->model, $this->attribute, $this->errorOptions); + } + if (!isset($this->parts['{hint}'])) { + $this->parts['{hint}'] = ''; + } + $content = strtr($this->template, $this->parts); + } elseif (!is_string($content)) { + $content = call_user_func($content, $this); + } + + return $this->begin() . "\n" . $content . "\n" . $this->end(); + } + + /** + * Renders the opening tag of the field container. + * @return string the rendering result. + */ + public function begin() + { + $clientOptions = $this->getClientOptions(); + if (!empty($clientOptions)) { + $this->form->attributes[$this->attribute] = $clientOptions; + } + + $inputID = Html::getInputId($this->model, $this->attribute); + $attribute = Html::getAttributeName($this->attribute); + $options = $this->options; + $class = isset($options['class']) ? [$options['class']] : []; + $class[] = "field-$inputID"; + if ($this->model->isAttributeRequired($attribute)) { + $class[] = $this->form->requiredCssClass; + } + if ($this->model->hasErrors($attribute)) { + $class[] = $this->form->errorCssClass; + } + $options['class'] = implode(' ', $class); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + + return Html::beginTag($tag, $options); + } + + /** + * Renders the closing tag of the field container. + * @return string the rendering result. + */ + public function end() + { + return Html::endTag(isset($this->options['tag']) ? $this->options['tag'] : 'div'); + } + + /** + * Generates a label tag for [[attribute]]. + * @param string $label the label to use. If null, it will be generated via [[Model::getAttributeLabel()]]. + * Note that this will NOT be [[Html::encode()|encoded]]. + * @param array $options the tag options in terms of name-value pairs. It will be merged with [[labelOptions]]. + * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded + * using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @return static the field object itself + */ + public function label($label = null, $options = []) + { + $options = array_merge($this->labelOptions, $options); + if ($label !== null) { + $options['label'] = $label; + } + $this->parts['{label}'] = Html::activeLabel($this->model, $this->attribute, $options); + + return $this; + } + + /** + * Generates a tag that contains the first validation error of [[attribute]]. + * Note that even if there is no validation error, this method will still return an empty error tag. + * @param array $options the tag options in terms of name-value pairs. It will be merged with [[errorOptions]]. + * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded + * using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * The following options are specially handled: + * + * - tag: this specifies the tag name. If not set, "div" will be used. + * + * @return static the field object itself + */ + public function error($options = []) + { + $options = array_merge($this->errorOptions, $options); + $this->parts['{error}'] = Html::error($this->model, $this->attribute, $options); + + return $this; + } + + /** + * Renders the hint tag. + * @param string $content the hint content. It will NOT be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the hint tag. The values will be HTML-encoded using [[Html::encode()]]. + * + * The following options are specially handled: + * + * - tag: this specifies the tag name. If not set, "div" will be used. + * + * @return static the field object itself + */ + public function hint($content, $options = []) + { + $options = array_merge($this->hintOptions, $options); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $this->parts['{hint}'] = Html::tag($tag, $content, $options); + + return $this; + } + + /** + * Renders an input tag. + * @param string $type the input type (e.g. 'text', 'password') + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself + */ + public function input($type, $options = []) + { + $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeInput($type, $this->model, $this->attribute, $options); + + return $this; + } + + /** + * Renders a text input. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself + */ + public function textInput($options = []) + { + $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeTextInput($this->model, $this->attribute, $options); + + return $this; + } + + /** + * Renders a password input. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself + */ + public function passwordInput($options = []) + { + $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activePasswordInput($this->model, $this->attribute, $options); + + return $this; + } + + /** + * Renders a file input. + * This method will generate the "name" and "value" tag attributes automatically for the model attribute + * unless they are explicitly specified in `$options`. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself + */ + public function fileInput($options = []) + { + // https://github.com/yiisoft/yii2/pull/795 + if ($this->inputOptions !== ['class' => 'form-control']) { + $options = array_merge($this->inputOptions, $options); + } + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeFileInput($this->model, $this->attribute, $options); + + return $this; + } + + /** + * Renders a text area. + * The model attribute value will be used as the content in the textarea. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[Html::encode()]]. + * @return static the field object itself + */ + public function textarea($options = []) + { + $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeTextarea($this->model, $this->attribute, $options); + + return $this; + } + + /** + * Renders a radio button. + * This method will generate the "checked" tag attribute according to the model attribute value. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, + * it will take the default value '0'. This method will render a hidden input so that if the radio button + * is not checked and is submitted, the value of this attribute will still be submitted to the server + * via the hidden input. + * - label: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can pass + * in HTML code such as an image tag. If this is is coming from end users, you should [[Html::encode()]] it to prevent XSS attacks. + * When this option is specified, the radio button will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @param boolean $enclosedByLabel whether to enclose the radio within the label. + * If true, the method will still use [[template]] to layout the checkbox and the error message + * except that the radio is enclosed by the label tag. + * @return static the field object itself + */ + public function radio($options = [], $enclosedByLabel = true) + { + if ($enclosedByLabel) { + if (!isset($options['label'])) { + $attribute = Html::getAttributeName($this->attribute); + $options['label'] = Html::encode($this->model->getAttributeLabel($attribute)); + } + $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); + $this->parts['{label}'] = ''; + } else { + $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); + } + $this->adjustLabelFor($options); + + return $this; + } + + /** + * Renders a checkbox. + * This method will generate the "checked" tag attribute according to the model attribute value. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. If not set, + * it will take the default value '0'. This method will render a hidden input so that if the radio button + * is not checked and is submitted, the value of this attribute will still be submitted to the server + * via the hidden input. + * - label: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass + * in HTML code such as an image tag. If this is is coming from end users, you should [[Html::encode()]] it to prevent XSS attacks. + * When this option is specified, the checkbox will be enclosed by a label tag. + * - labelOptions: array, the HTML attributes for the label tag. This is only used when the "label" option is specified. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @param boolean $enclosedByLabel whether to enclose the checkbox within the label. + * If true, the method will still use [[template]] to layout the checkbox and the error message + * except that the checkbox is enclosed by the label tag. + * @return static the field object itself + */ + public function checkbox($options = [], $enclosedByLabel = true) + { + if ($enclosedByLabel) { + if (!isset($options['label'])) { + $attribute = Html::getAttributeName($this->attribute); + $options['label'] = Html::encode($this->model->getAttributeLabel($attribute)); + } + $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); + $this->parts['{label}'] = ''; + } else { + $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); + } + $this->adjustLabelFor($options); + + return $this; + } + + /** + * Renders a drop-down list. + * The selection of the drop-down list is taken from the value of the model attribute. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * [ + * 'value1' => ['disabled' => true], + * 'value2' => ['label' => 'value 2'], + * ]; + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return static the field object itself + */ + public function dropDownList($items, $options = []) + { + $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeDropDownList($this->model, $this->attribute, $items, $options); + + return $this; + } + + /** + * Renders a list box. + * The selection of the list box is taken from the value of the model attribute. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * [ + * 'value1' => ['disabled' => true], + * 'value2' => ['label' => 'value 2'], + * ]; + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return static the field object itself + */ + public function listBox($items, $options = []) + { + $options = array_merge($this->inputOptions, $options); + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeListBox($this->model, $this->attribute, $items, $options); + + return $this; + } + + /** + * Renders a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * The selection of the checkbox list is taken from the value of the model attribute. + * @param array $items the data item used to generate the checkboxes. + * The array values are the labels, while the array keys are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are specially handled: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return static the field object itself + */ + public function checkboxList($items, $options = []) + { + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeCheckboxList($this->model, $this->attribute, $items, $options); + + return $this; + } + + /** + * Renders a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * The selection of the radio buttons is taken from the value of the model attribute. + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are specially handled: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return static the field object itself + */ + public function radioList($items, $options = []) + { + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeRadioList($this->model, $this->attribute, $items, $options); + + return $this; + } + + /** + * Renders a widget as the input of the field. + * + * Note that the widget must have both `model` and `attribute` properties. They will + * be initialized with [[model]] and [[attribute]] of this field, respectively. + * + * If you want to use a widget that does not have `model` and `attribute` properties, + * please use [[render()]] instead. + * + * @param string $class the widget class name + * @param array $config name-value pairs that will be used to initialize the widget + * @return static the field object itself + */ + public function widget($class, $config = []) + { + /** @var \yii\base\Widget $class */ + $config['model'] = $this->model; + $config['attribute'] = $this->attribute; + $config['view'] = $this->form->getView(); + $this->parts['{input}'] = $class::widget($config); + + return $this; + } + + /** + * Adjusts the "for" attribute for the label based on the input options. + * @param array $options the input options + */ + protected function adjustLabelFor($options) + { + if (isset($options['id']) && !isset($this->labelOptions['for'])) { + $this->labelOptions['for'] = $options['id']; + } + } + + /** + * Returns the JS options for the field. + * @return array the JS options + */ + protected function getClientOptions() + { + $attribute = Html::getAttributeName($this->attribute); + if (!in_array($attribute, $this->model->activeAttributes(), true)) { + return []; + } + + $options = []; + + $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; + if ($enableClientValidation) { + $validators = []; + foreach ($this->model->getActiveValidators($attribute) as $validator) { + /** @var \yii\validators\Validator $validator */ + $js = $validator->clientValidateAttribute($this->model, $attribute, $this->form->getView()); + if ($validator->enableClientValidation && $js != '') { + $validators[] = $js; + } + } + if (!empty($validators)) { + $options['validate'] = new JsExpression("function (attribute, value, messages) {" . implode('', $validators) . '}'); + } + } + + $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; + if ($enableAjaxValidation) { + $options['enableAjaxValidation'] = 1; + } + + if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { + $inputID = Html::getInputId($this->model, $this->attribute); + $options['name'] = $inputID; + foreach (['validateOnChange', 'validateOnType', 'validationDelay'] as $name) { + $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; + } + $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; + $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; + if (isset($this->errorOptions['class'])) { + $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); + } else { + $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span'; + } + + return $options; + } else { + return []; + } + } } diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 3a04e62af6a..06ead69ce23 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -23,340 +23,345 @@ */ class ActiveForm extends Widget { - /** - * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Url::to()]]. - */ - public $action = ''; - /** - * @var string the form submission method. This should be either 'post' or 'get'. - * Defaults to 'post'. - */ - public $method = 'post'; - /** - * @var array the HTML attributes (name-value pairs) for the form tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var array the default configuration used by [[field()]] when creating a new field object. - */ - public $fieldConfig; - /** - * @var string the default CSS class for the error summary container. - * @see errorSummary() - */ - public $errorSummaryCssClass = 'error-summary'; - /** - * @var string the CSS class that is added to a field container when the associated attribute is required. - */ - public $requiredCssClass = 'required'; - /** - * @var string the CSS class that is added to a field container when the associated attribute has validation error. - */ - public $errorCssClass = 'has-error'; - /** - * @var string the CSS class that is added to a field container when the associated attribute is successfully validated. - */ - public $successCssClass = 'has-success'; - /** - * @var string the CSS class that is added to a field container when the associated attribute is being validated. - */ - public $validatingCssClass = 'validating'; - /** - * @var boolean whether to enable client-side data validation. - * If [[ActiveField::enableClientValidation]] is set, its value will take precedence for that input field. - */ - public $enableClientValidation = true; - /** - * @var boolean whether to enable AJAX-based data validation. - * If [[ActiveField::enableAjaxValidation]] is set, its value will take precedence for that input field. - */ - public $enableAjaxValidation = false; - /** - * @var array|string the URL for performing AJAX-based validation. This property will be processed by - * [[Url::to()]]. Please refer to [[Url::to()]] for more details on how to configure this property. - * If this property is not set, it will take the value of the form's action attribute. - */ - public $validationUrl; - /** - * @var boolean whether to perform validation when the form is submitted. - */ - public $validateOnSubmit = true; - /** - * @var boolean whether to perform validation when an input field loses focus and its value is found changed. - * If [[ActiveField::validateOnChange]] is set, its value will take precedence for that input field. - */ - public $validateOnChange = true; - /** - * @var boolean whether to perform validation while the user is typing in an input field. - * If [[ActiveField::validateOnType]] is set, its value will take precedence for that input field. - * @see validationDelay - */ - public $validateOnType = false; - /** - * @var integer number of milliseconds that the validation should be delayed when an input field - * is changed or the user types in the field. - * If [[ActiveField::validationDelay]] is set, its value will take precedence for that input field. - */ - public $validationDelay = 200; - /** - * @var string the name of the GET parameter indicating the validation request is an AJAX request. - */ - public $ajaxParam = 'ajax'; - /** - * @var string the type of data that you're expecting back from the server. - */ - public $ajaxDataType = 'json'; - /** - * @var string|JsExpression a JS callback that will be called when the form is being submitted. - * The signature of the callback should be: - * - * ~~~ - * function ($form) { - * ...return false to cancel submission... - * } - * ~~~ - */ - public $beforeSubmit; - /** - * @var string|JsExpression a JS callback that is called before validating an attribute. - * The signature of the callback should be: - * - * ~~~ - * function ($form, attribute, messages) { - * ...return false to cancel the validation... - * } - * ~~~ - */ - public $beforeValidate; - /** - * @var string|JsExpression a JS callback that is called after validating an attribute. - * The signature of the callback should be: - * - * ~~~ - * function ($form, attribute, messages) { - * } - * ~~~ - */ - public $afterValidate; - /** - * @var array the client validation options for individual attributes. Each element of the array - * represents the validation options for a particular attribute. - * @internal - */ - public $attributes = []; + /** + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Url::to()]]. + */ + public $action = ''; + /** + * @var string the form submission method. This should be either 'post' or 'get'. + * Defaults to 'post'. + */ + public $method = 'post'; + /** + * @var array the HTML attributes (name-value pairs) for the form tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the default configuration used by [[field()]] when creating a new field object. + */ + public $fieldConfig; + /** + * @var string the default CSS class for the error summary container. + * @see errorSummary() + */ + public $errorSummaryCssClass = 'error-summary'; + /** + * @var string the CSS class that is added to a field container when the associated attribute is required. + */ + public $requiredCssClass = 'required'; + /** + * @var string the CSS class that is added to a field container when the associated attribute has validation error. + */ + public $errorCssClass = 'has-error'; + /** + * @var string the CSS class that is added to a field container when the associated attribute is successfully validated. + */ + public $successCssClass = 'has-success'; + /** + * @var string the CSS class that is added to a field container when the associated attribute is being validated. + */ + public $validatingCssClass = 'validating'; + /** + * @var boolean whether to enable client-side data validation. + * If [[ActiveField::enableClientValidation]] is set, its value will take precedence for that input field. + */ + public $enableClientValidation = true; + /** + * @var boolean whether to enable AJAX-based data validation. + * If [[ActiveField::enableAjaxValidation]] is set, its value will take precedence for that input field. + */ + public $enableAjaxValidation = false; + /** + * @var array|string the URL for performing AJAX-based validation. This property will be processed by + * [[Url::to()]]. Please refer to [[Url::to()]] for more details on how to configure this property. + * If this property is not set, it will take the value of the form's action attribute. + */ + public $validationUrl; + /** + * @var boolean whether to perform validation when the form is submitted. + */ + public $validateOnSubmit = true; + /** + * @var boolean whether to perform validation when an input field loses focus and its value is found changed. + * If [[ActiveField::validateOnChange]] is set, its value will take precedence for that input field. + */ + public $validateOnChange = true; + /** + * @var boolean whether to perform validation while the user is typing in an input field. + * If [[ActiveField::validateOnType]] is set, its value will take precedence for that input field. + * @see validationDelay + */ + public $validateOnType = false; + /** + * @var integer number of milliseconds that the validation should be delayed when an input field + * is changed or the user types in the field. + * If [[ActiveField::validationDelay]] is set, its value will take precedence for that input field. + */ + public $validationDelay = 200; + /** + * @var string the name of the GET parameter indicating the validation request is an AJAX request. + */ + public $ajaxParam = 'ajax'; + /** + * @var string the type of data that you're expecting back from the server. + */ + public $ajaxDataType = 'json'; + /** + * @var string|JsExpression a JS callback that will be called when the form is being submitted. + * The signature of the callback should be: + * + * ~~~ + * function ($form) { + * ...return false to cancel submission... + * } + * ~~~ + */ + public $beforeSubmit; + /** + * @var string|JsExpression a JS callback that is called before validating an attribute. + * The signature of the callback should be: + * + * ~~~ + * function ($form, attribute, messages) { + * ...return false to cancel the validation... + * } + * ~~~ + */ + public $beforeValidate; + /** + * @var string|JsExpression a JS callback that is called after validating an attribute. + * The signature of the callback should be: + * + * ~~~ + * function ($form, attribute, messages) { + * } + * ~~~ + */ + public $afterValidate; + /** + * @var array the client validation options for individual attributes. Each element of the array + * represents the validation options for a particular attribute. + * @internal + */ + public $attributes = []; - /** - * Initializes the widget. - * This renders the form open tag. - */ - public function init() - { - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - if (!isset($this->fieldConfig['class'])) { - $this->fieldConfig['class'] = ActiveField::className(); - } - echo Html::beginForm($this->action, $this->method, $this->options); - } + /** + * Initializes the widget. + * This renders the form open tag. + */ + public function init() + { + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + if (!isset($this->fieldConfig['class'])) { + $this->fieldConfig['class'] = ActiveField::className(); + } + echo Html::beginForm($this->action, $this->method, $this->options); + } - /** - * Runs the widget. - * This registers the necessary javascript code and renders the form close tag. - */ - public function run() - { - if (!empty($this->attributes)) { - $id = $this->options['id']; - $options = Json::encode($this->getClientOptions()); - $attributes = Json::encode($this->attributes); - $view = $this->getView(); - ActiveFormAsset::register($view); - $view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); - } - echo Html::endForm(); - } + /** + * Runs the widget. + * This registers the necessary javascript code and renders the form close tag. + */ + public function run() + { + if (!empty($this->attributes)) { + $id = $this->options['id']; + $options = Json::encode($this->getClientOptions()); + $attributes = Json::encode($this->attributes); + $view = $this->getView(); + ActiveFormAsset::register($view); + $view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); + } + echo Html::endForm(); + } - /** - * Returns the options for the form JS widget. - * @return array the options - */ - protected function getClientOptions() - { - $options = [ - 'errorSummary' => '.' . $this->errorSummaryCssClass, - 'validateOnSubmit' => $this->validateOnSubmit, - 'errorCssClass' => $this->errorCssClass, - 'successCssClass' => $this->successCssClass, - 'validatingCssClass' => $this->validatingCssClass, - 'ajaxParam' => $this->ajaxParam, - 'ajaxDataType' => $this->ajaxDataType, - ]; - if ($this->validationUrl !== null) { - $options['validationUrl'] = Url::to($this->validationUrl); - } - foreach (['beforeSubmit', 'beforeValidate', 'afterValidate'] as $name) { - if (($value = $this->$name) !== null) { - $options[$name] = $value instanceof JsExpression ? $value : new JsExpression($value); - } - } - return $options; - } + /** + * Returns the options for the form JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $options = [ + 'errorSummary' => '.' . $this->errorSummaryCssClass, + 'validateOnSubmit' => $this->validateOnSubmit, + 'errorCssClass' => $this->errorCssClass, + 'successCssClass' => $this->successCssClass, + 'validatingCssClass' => $this->validatingCssClass, + 'ajaxParam' => $this->ajaxParam, + 'ajaxDataType' => $this->ajaxDataType, + ]; + if ($this->validationUrl !== null) { + $options['validationUrl'] = Url::to($this->validationUrl); + } + foreach (['beforeSubmit', 'beforeValidate', 'afterValidate'] as $name) { + if (($value = $this->$name) !== null) { + $options[$name] = $value instanceof JsExpression ? $value : new JsExpression($value); + } + } - /** - * Generates a summary of the validation errors. - * If there is no validation error, an empty error summary markup will still be generated, but it will be hidden. - * @param Model|Model[] $models the model(s) associated with this form - * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: - * - * - header: string, the header HTML for the error summary. If not set, a default prompt string will be used. - * - footer: string, the footer HTML for the error summary. - * - * The rest of the options will be rendered as the attributes of the container tag. The values will - * be HTML-encoded using [[\yii\helpers\Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. - * @return string the generated error summary - */ - public function errorSummary($models, $options = []) - { - if (!is_array($models)) { - $models = [$models]; - } + return $options; + } - $lines = []; - foreach ($models as $model) { - /** @var Model $model */ - foreach ($model->getFirstErrors() as $error) { - $lines[] = Html::encode($error); - } - } + /** + * Generates a summary of the validation errors. + * If there is no validation error, an empty error summary markup will still be generated, but it will be hidden. + * @param Model|Model[] $models the model(s) associated with this form + * @param array $options the tag options in terms of name-value pairs. The following options are specially handled: + * + * - header: string, the header HTML for the error summary. If not set, a default prompt string will be used. + * - footer: string, the footer HTML for the error summary. + * + * The rest of the options will be rendered as the attributes of the container tag. The values will + * be HTML-encoded using [[\yii\helpers\Html::encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @return string the generated error summary + */ + public function errorSummary($models, $options = []) + { + if (!is_array($models)) { + $models = [$models]; + } - $header = isset($options['header']) ? $options['header'] : '

    ' . Yii::t('yii', 'Please fix the following errors:') . '

    '; - $footer = isset($options['footer']) ? $options['footer'] : ''; - unset($options['header'], $options['footer']); + $lines = []; + foreach ($models as $model) { + /** @var Model $model */ + foreach ($model->getFirstErrors() as $error) { + $lines[] = Html::encode($error); + } + } - if (!isset($options['class'])) { - $options['class'] = $this->errorSummaryCssClass; - } else { - $options['class'] .= ' ' . $this->errorSummaryCssClass; - } + $header = isset($options['header']) ? $options['header'] : '

    ' . Yii::t('yii', 'Please fix the following errors:') . '

    '; + $footer = isset($options['footer']) ? $options['footer'] : ''; + unset($options['header'], $options['footer']); - if (!empty($lines)) { - $content = "
    • " . implode("
    • \n
    • ", $lines) . "
    "; - return Html::tag('div', $header . $content . $footer, $options); - } else { - $content = "
      "; - $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; - return Html::tag('div', $header . $content . $footer, $options); - } - } + if (!isset($options['class'])) { + $options['class'] = $this->errorSummaryCssClass; + } else { + $options['class'] .= ' ' . $this->errorSummaryCssClass; + } - /** - * Generates a form field. - * A form field is associated with a model and an attribute. It contains a label, an input and an error message - * and use them to interact with end users to collect their inputs for the attribute. - * @param Model $model the data model - * @param string $attribute the attribute name or expression. See [[Html::getAttributeName()]] for the format - * about attribute expression. - * @param array $options the additional configurations for the field object - * @return ActiveField the created ActiveField object - * @see fieldConfig - */ - public function field($model, $attribute, $options = []) - { - return Yii::createObject(array_merge($this->fieldConfig, $options, [ - 'model' => $model, - 'attribute' => $attribute, - 'form' => $this, - ])); - } + if (!empty($lines)) { + $content = "
      • " . implode("
      • \n
      • ", $lines) . "
      "; - /** - * Validates one or several models and returns an error message array indexed by the attribute IDs. - * This is a helper method that simplifies the way of writing AJAX validation code. - * - * For example, you may use the following code in a controller action to respond - * to an AJAX validation request: - * - * ~~~ - * $model = new Post; - * $model->load($_POST); - * if (Yii::$app->request->isAjax) { - * Yii::$app->response->format = Response::FORMAT_JSON; - * return ActiveForm::validate($model); - * } - * // ... respond to non-AJAX request ... - * ~~~ - * - * To validate multiple models, simply pass each model as a parameter to this method, like - * the following: - * - * ~~~ - * ActiveForm::validate($model1, $model2, ...); - * ~~~ - * - * @param Model $model the model to be validated - * @param mixed $attributes list of attributes that should be validated. - * If this parameter is empty, it means any attribute listed in the applicable - * validation rules should be validated. - * - * When this method is used to validate multiple models, this parameter will be interpreted - * as a model. - * - * @return array the error message array indexed by the attribute IDs. - */ - public static function validate($model, $attributes = null) - { - $result = []; - if ($attributes instanceof Model) { - // validating multiple models - $models = func_get_args(); - $attributes = null; - } else { - $models = [$model]; - } - /** @var Model $model */ - foreach ($models as $model) { - $model->validate($attributes); - foreach ($model->getErrors() as $attribute => $errors) { - $result[Html::getInputId($model, $attribute)] = $errors; - } - } - return $result; - } + return Html::tag('div', $header . $content . $footer, $options); + } else { + $content = "
        "; + $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; - /** - * Validates an array of model instances and returns an error message array indexed by the attribute IDs. - * This is a helper method that simplifies the way of writing AJAX validation code for tabular input. - * - * For example, you may use the following code in a controller action to respond - * to an AJAX validation request: - * - * ~~~ - * // ... load $models ... - * if (Yii::$app->request->isAjax) { - * Yii::$app->response->format = Response::FORMAT_JSON; - * return ActiveForm::validateMultiple($models); - * } - * // ... respond to non-AJAX request ... - * ~~~ - * - * @param array $models an array of models to be validated. - * @param mixed $attributes list of attributes that should be validated. - * If this parameter is empty, it means any attribute listed in the applicable - * validation rules should be validated. - * @return array the error message array indexed by the attribute IDs. - */ - public static function validateMultiple($models, $attributes = null) - { - $result = []; - /** @var Model $model */ - foreach ($models as $i => $model) { - $model->validate($attributes); - foreach ($model->getErrors() as $attribute => $errors) { - $result[Html::getInputId($model, "[$i]" . $attribute)] = $errors; - } - } - return $result; - } + return Html::tag('div', $header . $content . $footer, $options); + } + } + + /** + * Generates a form field. + * A form field is associated with a model and an attribute. It contains a label, an input and an error message + * and use them to interact with end users to collect their inputs for the attribute. + * @param Model $model the data model + * @param string $attribute the attribute name or expression. See [[Html::getAttributeName()]] for the format + * about attribute expression. + * @param array $options the additional configurations for the field object + * @return ActiveField the created ActiveField object + * @see fieldConfig + */ + public function field($model, $attribute, $options = []) + { + return Yii::createObject(array_merge($this->fieldConfig, $options, [ + 'model' => $model, + 'attribute' => $attribute, + 'form' => $this, + ])); + } + + /** + * Validates one or several models and returns an error message array indexed by the attribute IDs. + * This is a helper method that simplifies the way of writing AJAX validation code. + * + * For example, you may use the following code in a controller action to respond + * to an AJAX validation request: + * + * ~~~ + * $model = new Post; + * $model->load($_POST); + * if (Yii::$app->request->isAjax) { + * Yii::$app->response->format = Response::FORMAT_JSON; + * return ActiveForm::validate($model); + * } + * // ... respond to non-AJAX request ... + * ~~~ + * + * To validate multiple models, simply pass each model as a parameter to this method, like + * the following: + * + * ~~~ + * ActiveForm::validate($model1, $model2, ...); + * ~~~ + * + * @param Model $model the model to be validated + * @param mixed $attributes list of attributes that should be validated. + * If this parameter is empty, it means any attribute listed in the applicable + * validation rules should be validated. + * + * When this method is used to validate multiple models, this parameter will be interpreted + * as a model. + * + * @return array the error message array indexed by the attribute IDs. + */ + public static function validate($model, $attributes = null) + { + $result = []; + if ($attributes instanceof Model) { + // validating multiple models + $models = func_get_args(); + $attributes = null; + } else { + $models = [$model]; + } + /** @var Model $model */ + foreach ($models as $model) { + $model->validate($attributes); + foreach ($model->getErrors() as $attribute => $errors) { + $result[Html::getInputId($model, $attribute)] = $errors; + } + } + + return $result; + } + + /** + * Validates an array of model instances and returns an error message array indexed by the attribute IDs. + * This is a helper method that simplifies the way of writing AJAX validation code for tabular input. + * + * For example, you may use the following code in a controller action to respond + * to an AJAX validation request: + * + * ~~~ + * // ... load $models ... + * if (Yii::$app->request->isAjax) { + * Yii::$app->response->format = Response::FORMAT_JSON; + * return ActiveForm::validateMultiple($models); + * } + * // ... respond to non-AJAX request ... + * ~~~ + * + * @param array $models an array of models to be validated. + * @param mixed $attributes list of attributes that should be validated. + * If this parameter is empty, it means any attribute listed in the applicable + * validation rules should be validated. + * @return array the error message array indexed by the attribute IDs. + */ + public static function validateMultiple($models, $attributes = null) + { + $result = []; + /** @var Model $model */ + foreach ($models as $i => $model) { + $model->validate($attributes); + foreach ($model->getErrors() as $attribute => $errors) { + $result[Html::getInputId($model, "[$i]" . $attribute)] = $errors; + } + } + + return $result; + } } diff --git a/framework/widgets/ActiveFormAsset.php b/framework/widgets/ActiveFormAsset.php index 94b00e5143e..971606318a1 100644 --- a/framework/widgets/ActiveFormAsset.php +++ b/framework/widgets/ActiveFormAsset.php @@ -15,11 +15,11 @@ */ class ActiveFormAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'yii.activeForm.js', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'yii.activeForm.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/framework/widgets/BaseListView.php b/framework/widgets/BaseListView.php index 8fddf4f69c7..14398b90ceb 100644 --- a/framework/widgets/BaseListView.php +++ b/framework/widgets/BaseListView.php @@ -19,216 +19,219 @@ */ abstract class BaseListView extends Widget { - /** - * @var array the HTML attributes for the container tag of the list view. - * The "tag" element specifies the tag name of the container element and defaults to "div". - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var \yii\data\DataProviderInterface the data provider for the view. This property is required. - */ - public $dataProvider; - /** - * @var array the configuration for the pager widget. By default, [[LinkPager]] will be - * used to render the pager. You can use a different widget class by configuring the "class" element. - */ - public $pager = []; - /** - * @var array the configuration for the sorter widget. By default, [[LinkSorter]] will be - * used to render the sorter. You can use a different widget class by configuring the "class" element. - */ - public $sorter = []; - /** - * @var string the HTML content to be displayed as the summary of the list view. - * If you do not want to show the summary, you may set it with an empty string. - * - * The following tokens will be replaced with the corresponding values: - * - * - `{begin}`: the starting row number (1-based) currently being displayed - * - `{end}`: the ending row number (1-based) currently being displayed - * - `{count}`: the number of rows currently being displayed - * - `{totalCount}`: the total number of rows available - * - `{page}`: the page number (1-based) current being displayed - * - `{pageCount}`: the number of pages available - */ - public $summary; - /** - * @var boolean whether to show the list view if [[dataProvider]] returns no data. - */ - public $showOnEmpty = false; - /** - * @var string the HTML content to be displayed when [[dataProvider]] does not have any data. - */ - public $emptyText; - /** - * @var string the layout that determines how different sections of the list view should be organized. - * The following tokens will be replaced with the corresponding section contents: - * - * - `{summary}`: the summary section. See [[renderSummary()]]. - * - `{items}`: the list items. See [[renderItems()]]. - * - `{sorter}`: the sorter. See [[renderSorter()]]. - * - `{pager}`: the pager. See [[renderPager()]]. - */ - public $layout = "{summary}\n{items}\n{pager}"; + /** + * @var array the HTML attributes for the container tag of the list view. + * The "tag" element specifies the tag name of the container element and defaults to "div". + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var \yii\data\DataProviderInterface the data provider for the view. This property is required. + */ + public $dataProvider; + /** + * @var array the configuration for the pager widget. By default, [[LinkPager]] will be + * used to render the pager. You can use a different widget class by configuring the "class" element. + */ + public $pager = []; + /** + * @var array the configuration for the sorter widget. By default, [[LinkSorter]] will be + * used to render the sorter. You can use a different widget class by configuring the "class" element. + */ + public $sorter = []; + /** + * @var string the HTML content to be displayed as the summary of the list view. + * If you do not want to show the summary, you may set it with an empty string. + * + * The following tokens will be replaced with the corresponding values: + * + * - `{begin}`: the starting row number (1-based) currently being displayed + * - `{end}`: the ending row number (1-based) currently being displayed + * - `{count}`: the number of rows currently being displayed + * - `{totalCount}`: the total number of rows available + * - `{page}`: the page number (1-based) current being displayed + * - `{pageCount}`: the number of pages available + */ + public $summary; + /** + * @var boolean whether to show the list view if [[dataProvider]] returns no data. + */ + public $showOnEmpty = false; + /** + * @var string the HTML content to be displayed when [[dataProvider]] does not have any data. + */ + public $emptyText; + /** + * @var string the layout that determines how different sections of the list view should be organized. + * The following tokens will be replaced with the corresponding section contents: + * + * - `{summary}`: the summary section. See [[renderSummary()]]. + * - `{items}`: the list items. See [[renderItems()]]. + * - `{sorter}`: the sorter. See [[renderSorter()]]. + * - `{pager}`: the pager. See [[renderPager()]]. + */ + public $layout = "{summary}\n{items}\n{pager}"; + /** + * Renders the data models. + * @return string the rendering result. + */ + abstract public function renderItems(); - /** - * Renders the data models. - * @return string the rendering result. - */ - abstract public function renderItems(); + /** + * Initializes the view. + */ + public function init() + { + if ($this->dataProvider === null) { + throw new InvalidConfigException('The "dataProvider" property must be set.'); + } + if ($this->emptyText === null) { + $this->emptyText = Yii::t('yii', 'No results found.'); + } + $this->dataProvider->prepare(); + } - /** - * Initializes the view. - */ - public function init() - { - if ($this->dataProvider === null) { - throw new InvalidConfigException('The "dataProvider" property must be set.'); - } - if ($this->emptyText === null) { - $this->emptyText = Yii::t('yii', 'No results found.'); - } - $this->dataProvider->prepare(); - } + /** + * Runs the widget. + */ + public function run() + { + if ($this->dataProvider->getCount() > 0 || $this->showOnEmpty) { + $content = preg_replace_callback("/{\\w+}/", function ($matches) { + $content = $this->renderSection($matches[0]); - /** - * Runs the widget. - */ - public function run() - { - if ($this->dataProvider->getCount() > 0 || $this->showOnEmpty) { - $content = preg_replace_callback("/{\\w+}/", function ($matches) { - $content = $this->renderSection($matches[0]); - return $content === false ? $matches[0] : $content; - }, $this->layout); - } else { - $content = $this->renderEmpty(); - } - $tag = ArrayHelper::remove($this->options, 'tag', 'div'); - echo Html::tag($tag, $content, $this->options); - } + return $content === false ? $matches[0] : $content; + }, $this->layout); + } else { + $content = $this->renderEmpty(); + } + $tag = ArrayHelper::remove($this->options, 'tag', 'div'); + echo Html::tag($tag, $content, $this->options); + } - /** - * Renders a section of the specified name. - * If the named section is not supported, false will be returned. - * @param string $name the section name, e.g., `{summary}`, `{items}`. - * @return string|boolean the rendering result of the section, or false if the named section is not supported. - */ - public function renderSection($name) - { - switch ($name) { - case '{summary}': - return $this->renderSummary(); - case '{items}': - return $this->renderItems(); - case '{pager}': - return $this->renderPager(); - case '{sorter}': - return $this->renderSorter(); - default: - return false; - } - } + /** + * Renders a section of the specified name. + * If the named section is not supported, false will be returned. + * @param string $name the section name, e.g., `{summary}`, `{items}`. + * @return string|boolean the rendering result of the section, or false if the named section is not supported. + */ + public function renderSection($name) + { + switch ($name) { + case '{summary}': + return $this->renderSummary(); + case '{items}': + return $this->renderItems(); + case '{pager}': + return $this->renderPager(); + case '{sorter}': + return $this->renderSorter(); + default: + return false; + } + } - /** - * Renders the HTML content indicating that the list view has no data. - * @return string the rendering result - * @see emptyText - */ - public function renderEmpty() - { - return '
        ' . ($this->emptyText === null ? Yii::t('yii', 'No results found.') : $this->emptyText) . '
        '; - } + /** + * Renders the HTML content indicating that the list view has no data. + * @return string the rendering result + * @see emptyText + */ + public function renderEmpty() + { + return '
        ' . ($this->emptyText === null ? Yii::t('yii', 'No results found.') : $this->emptyText) . '
        '; + } - /** - * Renders the summary text. - */ - public function renderSummary() - { - $count = $this->dataProvider->getCount(); - if ($count <= 0) { - return ''; - } - if (($pagination = $this->dataProvider->getPagination()) !== false) { - $totalCount = $this->dataProvider->getTotalCount(); - $begin = $pagination->getPage() * $pagination->pageSize + 1; - $end = $begin + $count - 1; - if ($begin > $end) { - $begin = $end; - } - $page = $pagination->getPage() + 1; - $pageCount = $pagination->pageCount; - if (($summaryContent = $this->summary) === null) { - return '
        ' - . Yii::t('yii', 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', [ - 'begin' => $begin, - 'end' => $end, - 'count' => $count, - 'totalCount' => $totalCount, - 'page' => $page, - 'pageCount' => $pageCount, - ]) - . '
        '; - } - } else { - $begin = $page = $pageCount = 1; - $end = $totalCount = $count; - if (($summaryContent = $this->summary) === null) { - return '
        ' . Yii::t('yii', 'Total {count, number} {count, plural, one{item} other{items}}.', [ - 'begin' => $begin, - 'end' => $end, - 'count' => $count, - 'totalCount' => $totalCount, - 'page' => $page, - 'pageCount' => $pageCount, - ]) . '
        '; - } - } - return Yii::$app->getI18n()->format($summaryContent, [ - 'begin' => $begin, - 'end' => $end, - 'count' => $count, - 'totalCount' => $totalCount, - 'page' => $page, - 'pageCount' => $pageCount, - ], Yii::$app->language); - } + /** + * Renders the summary text. + */ + public function renderSummary() + { + $count = $this->dataProvider->getCount(); + if ($count <= 0) { + return ''; + } + if (($pagination = $this->dataProvider->getPagination()) !== false) { + $totalCount = $this->dataProvider->getTotalCount(); + $begin = $pagination->getPage() * $pagination->pageSize + 1; + $end = $begin + $count - 1; + if ($begin > $end) { + $begin = $end; + } + $page = $pagination->getPage() + 1; + $pageCount = $pagination->pageCount; + if (($summaryContent = $this->summary) === null) { + return '
        ' + . Yii::t('yii', 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', [ + 'begin' => $begin, + 'end' => $end, + 'count' => $count, + 'totalCount' => $totalCount, + 'page' => $page, + 'pageCount' => $pageCount, + ]) + . '
        '; + } + } else { + $begin = $page = $pageCount = 1; + $end = $totalCount = $count; + if (($summaryContent = $this->summary) === null) { + return '
        ' . Yii::t('yii', 'Total {count, number} {count, plural, one{item} other{items}}.', [ + 'begin' => $begin, + 'end' => $end, + 'count' => $count, + 'totalCount' => $totalCount, + 'page' => $page, + 'pageCount' => $pageCount, + ]) . '
        '; + } + } - /** - * Renders the pager. - * @return string the rendering result - */ - public function renderPager() - { - $pagination = $this->dataProvider->getPagination(); - if ($pagination === false || $this->dataProvider->getCount() <= 0) { - return ''; - } - /** @var LinkPager $class */ - $class = ArrayHelper::remove($this->pager, 'class', LinkPager::className()); - $pager = $this->pager; - $pager['pagination'] = $pagination; - $pager['view'] = $this->getView(); - return $class::widget($pager); - } + return Yii::$app->getI18n()->format($summaryContent, [ + 'begin' => $begin, + 'end' => $end, + 'count' => $count, + 'totalCount' => $totalCount, + 'page' => $page, + 'pageCount' => $pageCount, + ], Yii::$app->language); + } - /** - * Renders the sorter. - * @return string the rendering result - */ - public function renderSorter() - { - $sort = $this->dataProvider->getSort(); - if ($sort === false || empty($sort->attributes) || $this->dataProvider->getCount() <= 0) { - return ''; - } - /** @var LinkSorter $class */ - $class = ArrayHelper::remove($this->sorter, 'class', LinkSorter::className()); - $sorter = $this->sorter; - $sorter['sort'] = $sort; - $sorter['view'] = $this->getView(); - return $class::widget($sorter); - } + /** + * Renders the pager. + * @return string the rendering result + */ + public function renderPager() + { + $pagination = $this->dataProvider->getPagination(); + if ($pagination === false || $this->dataProvider->getCount() <= 0) { + return ''; + } + /** @var LinkPager $class */ + $class = ArrayHelper::remove($this->pager, 'class', LinkPager::className()); + $pager = $this->pager; + $pager['pagination'] = $pagination; + $pager['view'] = $this->getView(); + + return $class::widget($pager); + } + + /** + * Renders the sorter. + * @return string the rendering result + */ + public function renderSorter() + { + $sort = $this->dataProvider->getSort(); + if ($sort === false || empty($sort->attributes) || $this->dataProvider->getCount() <= 0) { + return ''; + } + /** @var LinkSorter $class */ + $class = ArrayHelper::remove($this->sorter, 'class', LinkSorter::className()); + $sorter = $this->sorter; + $sorter['sort'] = $sort; + $sorter['view'] = $this->getView(); + + return $class::widget($sorter); + } } diff --git a/framework/widgets/Block.php b/framework/widgets/Block.php index 4eec21794cc..c1813ff806d 100644 --- a/framework/widgets/Block.php +++ b/framework/widgets/Block.php @@ -15,31 +15,31 @@ */ class Block extends Widget { - /** - * @var boolean whether to render the block content in place. Defaults to false, - * meaning the captured block content will not be displayed. - */ - public $renderInPlace = false; + /** + * @var boolean whether to render the block content in place. Defaults to false, + * meaning the captured block content will not be displayed. + */ + public $renderInPlace = false; - /** - * Starts recording a block. - */ - public function init() - { - ob_start(); - ob_implicit_flush(false); - } + /** + * Starts recording a block. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } - /** - * Ends recording a block. - * This method stops output buffering and saves the rendering result as a named block in the view. - */ - public function run() - { - $block = ob_get_clean(); - if ($this->renderInPlace) { - echo $block; - } - $this->view->blocks[$this->getId()] = $block; - } + /** + * Ends recording a block. + * This method stops output buffering and saves the rendering result as a named block in the view. + */ + public function run() + { + $block = ob_get_clean(); + if ($this->renderInPlace) { + echo $block; + } + $this->view->blocks[$this->getId()] = $block; + } } diff --git a/framework/widgets/Breadcrumbs.php b/framework/widgets/Breadcrumbs.php index 195ef07523f..e4936373373 100644 --- a/framework/widgets/Breadcrumbs.php +++ b/framework/widgets/Breadcrumbs.php @@ -47,97 +47,97 @@ */ class Breadcrumbs extends Widget { - /** - * @var string the name of the breadcrumb container tag. - */ - public $tag = 'ul'; - /** - * @var array the HTML attributes for the breadcrumb container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'breadcrumb']; - /** - * @var boolean whether to HTML-encode the link labels. - */ - public $encodeLabels = true; - /** - * @var array the first hyperlink in the breadcrumbs (called home link). - * Please refer to [[links]] on the format of the link. - * If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]] - * with the label 'Home'. If this property is false, the home link will not be rendered. - */ - public $homeLink; - /** - * @var array list of links to appear in the breadcrumbs. If this property is empty, - * the widget will not render anything. Each array element represents a single link in the breadcrumbs - * with the following structure: - * - * ~~~ - * [ - * 'label' => 'label of the link', // required - * 'url' => 'url of the link', // optional, will be processed by Url::to() - * ] - * ~~~ - * - * If a link is active, you only need to specify its "label", and instead of writing `['label' => $label]`, - * you should simply use `$label`. - */ - public $links = []; - /** - * @var string the template used to render each inactive item in the breadcrumbs. The token `{link}` - * will be replaced with the actual HTML link for each inactive item. - */ - public $itemTemplate = "
      • {link}
      • \n"; - /** - * @var string the template used to render each active item in the breadcrumbs. The token `{link}` - * will be replaced with the actual HTML link for each active item. - */ - public $activeItemTemplate = "
      • {link}
      • \n"; + /** + * @var string the name of the breadcrumb container tag. + */ + public $tag = 'ul'; + /** + * @var array the HTML attributes for the breadcrumb container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'breadcrumb']; + /** + * @var boolean whether to HTML-encode the link labels. + */ + public $encodeLabels = true; + /** + * @var array the first hyperlink in the breadcrumbs (called home link). + * Please refer to [[links]] on the format of the link. + * If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]] + * with the label 'Home'. If this property is false, the home link will not be rendered. + */ + public $homeLink; + /** + * @var array list of links to appear in the breadcrumbs. If this property is empty, + * the widget will not render anything. Each array element represents a single link in the breadcrumbs + * with the following structure: + * + * ~~~ + * [ + * 'label' => 'label of the link', // required + * 'url' => 'url of the link', // optional, will be processed by Url::to() + * ] + * ~~~ + * + * If a link is active, you only need to specify its "label", and instead of writing `['label' => $label]`, + * you should simply use `$label`. + */ + public $links = []; + /** + * @var string the template used to render each inactive item in the breadcrumbs. The token `{link}` + * will be replaced with the actual HTML link for each inactive item. + */ + public $itemTemplate = "
      • {link}
      • \n"; + /** + * @var string the template used to render each active item in the breadcrumbs. The token `{link}` + * will be replaced with the actual HTML link for each active item. + */ + public $activeItemTemplate = "
      • {link}
      • \n"; - /** - * Renders the widget. - */ - public function run() - { - if (empty($this->links)) { - return; - } - $links = []; - if ($this->homeLink === null) { - $links[] = $this->renderItem([ - 'label' => Yii::t('yii', 'Home'), - 'url' => Yii::$app->homeUrl, - ], $this->itemTemplate); - } elseif ($this->homeLink !== false) { - $links[] = $this->renderItem($this->homeLink, $this->itemTemplate); - } - foreach ($this->links as $link) { - if (!is_array($link)) { - $link = ['label' => $link]; - } - $links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate); - } - echo Html::tag($this->tag, implode('', $links), $this->options); - } + /** + * Renders the widget. + */ + public function run() + { + if (empty($this->links)) { + return; + } + $links = []; + if ($this->homeLink === null) { + $links[] = $this->renderItem([ + 'label' => Yii::t('yii', 'Home'), + 'url' => Yii::$app->homeUrl, + ], $this->itemTemplate); + } elseif ($this->homeLink !== false) { + $links[] = $this->renderItem($this->homeLink, $this->itemTemplate); + } + foreach ($this->links as $link) { + if (!is_array($link)) { + $link = ['label' => $link]; + } + $links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate); + } + echo Html::tag($this->tag, implode('', $links), $this->options); + } - /** - * Renders a single breadcrumb item. - * @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional. - * @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the link. - * @return string the rendering result - * @throws InvalidConfigException if `$link` does not have "label" element. - */ - protected function renderItem($link, $template) - { - if (isset($link['label'])) { - $label = $this->encodeLabels ? Html::encode($link['label']) : $link['label']; - } else { - throw new InvalidConfigException('The "label" element is required for each link.'); - } - if (isset($link['url'])) { - return strtr($template, ['{link}' => Html::a($label, $link['url'])]); - } else { - return strtr($template, ['{link}' => $label]); - } - } + /** + * Renders a single breadcrumb item. + * @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional. + * @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the link. + * @return string the rendering result + * @throws InvalidConfigException if `$link` does not have "label" element. + */ + protected function renderItem($link, $template) + { + if (isset($link['label'])) { + $label = $this->encodeLabels ? Html::encode($link['label']) : $link['label']; + } else { + throw new InvalidConfigException('The "label" element is required for each link.'); + } + if (isset($link['url'])) { + return strtr($template, ['{link}' => Html::a($label, $link['url'])]); + } else { + return strtr($template, ['{link}' => $label]); + } + } } diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php index 9224f3551a7..160cd84d77e 100644 --- a/framework/widgets/ContentDecorator.php +++ b/framework/widgets/ContentDecorator.php @@ -16,37 +16,37 @@ */ class ContentDecorator extends Widget { - /** - * @var string the view file that will be used to decorate the content enclosed by this widget. - * This can be specified as either the view file path or path alias. - */ - public $viewFile; - /** - * @var array the parameters (name => value) to be extracted and made available in the decorative view. - */ - public $params = []; + /** + * @var string the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. + */ + public $viewFile; + /** + * @var array the parameters (name => value) to be extracted and made available in the decorative view. + */ + public $params = []; - /** - * Starts recording a clip. - */ - public function init() - { - if ($this->viewFile === null) { - throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); - } - ob_start(); - ob_implicit_flush(false); - } + /** + * Starts recording a clip. + */ + public function init() + { + if ($this->viewFile === null) { + throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); + } + ob_start(); + ob_implicit_flush(false); + } - /** - * Ends recording a clip. - * This method stops output buffering and saves the rendering result as a named clip in the controller. - */ - public function run() - { - $params = $this->params; - $params['content'] = ob_get_clean(); - // render under the existing context - echo $this->view->renderFile($this->viewFile, $params); - } + /** + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. + */ + public function run() + { + $params = $this->params; + $params['content'] = ob_get_clean(); + // render under the existing context + echo $this->view->renderFile($this->viewFile, $params); + } } diff --git a/framework/widgets/DetailView.php b/framework/widgets/DetailView.php index e97b5763390..00987c21f11 100644 --- a/framework/widgets/DetailView.php +++ b/framework/widgets/DetailView.php @@ -48,170 +48,169 @@ */ class DetailView extends Widget { - /** - * @var array|object the data model whose details are to be displayed. This can be either a [[Model]] instance - * or an associative array. - */ - public $model; - /** - * @var array a list of attributes to be displayed in the detail view. Each array element - * represents the specification for displaying one particular attribute. - * - * An attribute can be specified as a string in the format of "attribute", "attribute:format" or "attribute:format:label", - * where "attribute" refers to the attribute name, and "format" represents the format of the attribute. The "format" - * is passed to the [[Formatter::format()]] method to format an attribute value into a displayable text. - * Please refer to [[Formatter]] for the supported types. Both "format" and "label" are optional. - * They will take default values if absent. - * - * An attribute can also be specified in terms of an array with the following elements: - * - * - attribute: the attribute name. This is required if either "label" or "value" is not specified. - * - label: the label associated with the attribute. If this is not specified, it will be generated from the attribute name. - * - value: the value to be displayed. If this is not specified, it will be retrieved from [[model]] using the attribute name - * by calling [[ArrayHelper::getValue()]]. Note that this value will be formatted into a displayable text - * according to the "format" option. - * - format: the type of the value that determines how the value would be formatted into a displayable text. - * Please refer to [[Formatter]] for supported types. - * - visible: whether the attribute is visible. If set to `false`, the attribute will NOT be displayed. - */ - public $attributes; - /** - * @var string|callable the template used to render a single attribute. If a string, the token `{label}` - * and `{value}` will be replaced with the label and the value of the corresponding attribute. - * If a callback (e.g. an anonymous function), the signature must be as follows: - * - * ~~~ - * function ($attribute, $index, $widget) - * ~~~ - * - * where `$attribute` refer to the specification of the attribute being rendered, `$index` is the zero-based - * index of the attribute in the [[attributes]] array, and `$widget` refers to this widget instance. - */ - public $template = "{label}{value}"; - /** - * @var array the HTML attributes for the container tag of this widget. The "tag" option specifies - * what container tag should be used. It defaults to "table" if not set. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'table table-striped table-bordered detail-view']; - /** - * @var array|Formatter the formatter used to format model attribute values into displayable texts. - * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] - * instance. If this property is not set, the "formatter" application component will be used. - */ - public $formatter; + /** + * @var array|object the data model whose details are to be displayed. This can be either a [[Model]] instance + * or an associative array. + */ + public $model; + /** + * @var array a list of attributes to be displayed in the detail view. Each array element + * represents the specification for displaying one particular attribute. + * + * An attribute can be specified as a string in the format of "attribute", "attribute:format" or "attribute:format:label", + * where "attribute" refers to the attribute name, and "format" represents the format of the attribute. The "format" + * is passed to the [[Formatter::format()]] method to format an attribute value into a displayable text. + * Please refer to [[Formatter]] for the supported types. Both "format" and "label" are optional. + * They will take default values if absent. + * + * An attribute can also be specified in terms of an array with the following elements: + * + * - attribute: the attribute name. This is required if either "label" or "value" is not specified. + * - label: the label associated with the attribute. If this is not specified, it will be generated from the attribute name. + * - value: the value to be displayed. If this is not specified, it will be retrieved from [[model]] using the attribute name + * by calling [[ArrayHelper::getValue()]]. Note that this value will be formatted into a displayable text + * according to the "format" option. + * - format: the type of the value that determines how the value would be formatted into a displayable text. + * Please refer to [[Formatter]] for supported types. + * - visible: whether the attribute is visible. If set to `false`, the attribute will NOT be displayed. + */ + public $attributes; + /** + * @var string|callable the template used to render a single attribute. If a string, the token `{label}` + * and `{value}` will be replaced with the label and the value of the corresponding attribute. + * If a callback (e.g. an anonymous function), the signature must be as follows: + * + * ~~~ + * function ($attribute, $index, $widget) + * ~~~ + * + * where `$attribute` refer to the specification of the attribute being rendered, `$index` is the zero-based + * index of the attribute in the [[attributes]] array, and `$widget` refers to this widget instance. + */ + public $template = "{label}{value}"; + /** + * @var array the HTML attributes for the container tag of this widget. The "tag" option specifies + * what container tag should be used. It defaults to "table" if not set. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'table table-striped table-bordered detail-view']; + /** + * @var array|Formatter the formatter used to format model attribute values into displayable texts. + * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] + * instance. If this property is not set, the "formatter" application component will be used. + */ + public $formatter; + /** + * Initializes the detail view. + * This method will initialize required property values. + */ + public function init() + { + if ($this->model === null) { + throw new InvalidConfigException('Please specify the "model" property.'); + } + if ($this->formatter == null) { + $this->formatter = Yii::$app->getFormatter(); + } elseif (is_array($this->formatter)) { + $this->formatter = Yii::createObject($this->formatter); + } + if (!$this->formatter instanceof Formatter) { + throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); + } + $this->normalizeAttributes(); + } - /** - * Initializes the detail view. - * This method will initialize required property values. - */ - public function init() - { - if ($this->model === null) { - throw new InvalidConfigException('Please specify the "model" property.'); - } - if ($this->formatter == null) { - $this->formatter = Yii::$app->getFormatter(); - } elseif (is_array($this->formatter)) { - $this->formatter = Yii::createObject($this->formatter); - } - if (!$this->formatter instanceof Formatter) { - throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); - } - $this->normalizeAttributes(); - } + /** + * Renders the detail view. + * This is the main entry of the whole detail view rendering. + */ + public function run() + { + $rows = []; + $i = 0; + foreach ($this->attributes as $attribute) { + $rows[] = $this->renderAttribute($attribute, $i++); + } - /** - * Renders the detail view. - * This is the main entry of the whole detail view rendering. - */ - public function run() - { - $rows = []; - $i = 0; - foreach ($this->attributes as $attribute) { - $rows[] = $this->renderAttribute($attribute, $i++); - } + $tag = ArrayHelper::remove($this->options, 'tag', 'table'); + echo Html::tag($tag, implode("\n", $rows), $this->options); + } - $tag = ArrayHelper::remove($this->options, 'tag', 'table'); - echo Html::tag($tag, implode("\n", $rows), $this->options); - } + /** + * Renders a single attribute. + * @param array $attribute the specification of the attribute to be rendered. + * @param integer $index the zero-based index of the attribute in the [[attributes]] array + * @return string the rendering result + */ + protected function renderAttribute($attribute, $index) + { + if (is_string($this->template)) { + return strtr($this->template, [ + '{label}' => $attribute['label'], + '{value}' => $this->formatter->format($attribute['value'], $attribute['format']), + ]); + } else { + return call_user_func($this->template, $attribute, $index, $this); + } + } - /** - * Renders a single attribute. - * @param array $attribute the specification of the attribute to be rendered. - * @param integer $index the zero-based index of the attribute in the [[attributes]] array - * @return string the rendering result - */ - protected function renderAttribute($attribute, $index) - { - if (is_string($this->template)) { - return strtr($this->template, [ - '{label}' => $attribute['label'], - '{value}' => $this->formatter->format($attribute['value'], $attribute['format']), - ]); - } else { - return call_user_func($this->template, $attribute, $index, $this); - } - } + /** + * Normalizes the attribute specifications. + * @throws InvalidConfigException + */ + protected function normalizeAttributes() + { + if ($this->attributes === null) { + if ($this->model instanceof Model) { + $this->attributes = $this->model->attributes(); + } elseif (is_object($this->model)) { + $this->attributes = $this->model instanceof Arrayable ? $this->model->toArray() : array_keys(get_object_vars($this->model)); + } elseif (is_array($this->model)) { + $this->attributes = array_keys($this->model); + } else { + throw new InvalidConfigException('The "model" property must be either an array or an object.'); + } + sort($this->attributes); + } - /** - * Normalizes the attribute specifications. - * @throws InvalidConfigException - */ - protected function normalizeAttributes() - { - if ($this->attributes === null) { - if ($this->model instanceof Model) { - $this->attributes = $this->model->attributes(); - } elseif (is_object($this->model)) { - $this->attributes = $this->model instanceof Arrayable ? $this->model->toArray() : array_keys(get_object_vars($this->model)); - } elseif (is_array($this->model)) { - $this->attributes = array_keys($this->model); - } else { - throw new InvalidConfigException('The "model" property must be either an array or an object.'); - } - sort($this->attributes); - } + foreach ($this->attributes as $i => $attribute) { + if (is_string($attribute)) { + if (!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/', $attribute, $matches)) { + throw new InvalidConfigException('The attribute must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"'); + } + $attribute = [ + 'attribute' => $matches[1], + 'format' => isset($matches[3]) ? $matches[3] : 'text', + 'label' => isset($matches[5]) ? $matches[5] : null, + ]; + } - foreach ($this->attributes as $i => $attribute) { - if (is_string($attribute)) { - if (!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/', $attribute, $matches)) { - throw new InvalidConfigException('The attribute must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"'); - } - $attribute = [ - 'attribute' => $matches[1], - 'format' => isset($matches[3]) ? $matches[3] : 'text', - 'label' => isset($matches[5]) ? $matches[5] : null, - ]; - } + if (!is_array($attribute)) { + throw new InvalidConfigException('The attribute configuration must be an array.'); + } - if (!is_array($attribute)) { - throw new InvalidConfigException('The attribute configuration must be an array.'); - } + if (isset($attribute['visible']) && !$attribute['visible']) { + unset($this->attributes[$i]); + continue; + } - if (isset($attribute['visible']) && !$attribute['visible']) { - unset($this->attributes[$i]); - continue; - } + if (!isset($attribute['format'])) { + $attribute['format'] = 'text'; + } + if (isset($attribute['attribute'])) { + $attributeName = $attribute['attribute']; + if (!isset($attribute['label'])) { + $attribute['label'] = $this->model instanceof Model ? $this->model->getAttributeLabel($attributeName) : Inflector::camel2words($attributeName, true); + } + if (!array_key_exists('value', $attribute)) { + $attribute['value'] = ArrayHelper::getValue($this->model, $attributeName); + } + } elseif (!isset($attribute['label']) || !array_key_exists('value', $attribute)) { + throw new InvalidConfigException('The attribute configuration requires the "attribute" element to determine the value and display label.'); + } - if (!isset($attribute['format'])) { - $attribute['format'] = 'text'; - } - if (isset($attribute['attribute'])) { - $attributeName = $attribute['attribute']; - if (!isset($attribute['label'])) { - $attribute['label'] = $this->model instanceof Model ? $this->model->getAttributeLabel($attributeName) : Inflector::camel2words($attributeName, true); - } - if (!array_key_exists('value', $attribute)) { - $attribute['value'] = ArrayHelper::getValue($this->model, $attributeName); - } - } elseif (!isset($attribute['label']) || !array_key_exists('value', $attribute)) { - throw new InvalidConfigException('The attribute configuration requires the "attribute" element to determine the value and display label.'); - } - - $this->attributes[$i] = $attribute; - } - } + $this->attributes[$i] = $attribute; + } + } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 57c4659f33d..30f7a289017 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -22,157 +22,160 @@ */ class FragmentCache extends Widget { - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * After the FragmentCache object is created, if you want to change this property, - * you should only assign it with a cache object. - */ - public $cache = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * [ - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ] - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * [ - * Yii::$app->language, - * ] - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. - */ - public $dynamicPlaceholders; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * After the FragmentCache object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * [ + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ] + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * [ + * Yii::$app->language, + * ] + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. + */ + public $dynamicPlaceholders; - /** - * Initializes the FragmentCache object. - */ - public function init() - { - parent::init(); + /** + * Initializes the FragmentCache object. + */ + public function init() + { + parent::init(); - if (!$this->enabled) { - $this->cache = null; - } elseif (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } + if (!$this->enabled) { + $this->cache = null; + } elseif (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } - if ($this->getCachedContent() === false) { - $this->getView()->cacheStack[] = $this; - ob_start(); - ob_implicit_flush(false); - } - } + if ($this->getCachedContent() === false) { + $this->getView()->cacheStack[] = $this; + ob_start(); + ob_implicit_flush(false); + } + } - /** - * Marks the end of content to be cached. - * Content displayed before this method call and after [[init()]] - * will be captured and saved in cache. - * This method does nothing if valid content is already found in cache. - */ - public function run() - { - if (($content = $this->getCachedContent()) !== false) { - echo $content; - } elseif ($this->cache instanceof Cache) { - $content = ob_get_clean(); - array_pop($this->getView()->cacheStack); - if (is_array($this->dependency)) { - $this->dependency = Yii::createObject($this->dependency); - } - $data = [$content, $this->dynamicPlaceholders]; - $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after [[init()]] + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function run() + { + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif ($this->cache instanceof Cache) { + $content = ob_get_clean(); + array_pop($this->getView()->cacheStack); + if (is_array($this->dependency)) { + $this->dependency = Yii::createObject($this->dependency); + } + $data = [$content, $this->dynamicPlaceholders]; + $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - if (empty($this->getView()->cacheStack) && !empty($this->dynamicPlaceholders)) { - $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); - } - echo $content; - } - } + if (empty($this->getView()->cacheStack) && !empty($this->dynamicPlaceholders)) { + $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); + } + echo $content; + } + } - /** - * @var string|boolean the cached content. False if the content is not cached. - */ - private $_content; + /** + * @var string|boolean the cached content. False if the content is not cached. + */ + private $_content; - /** - * Returns the cached content if available. - * @return string|boolean the cached content. False is returned if valid content is not found in the cache. - */ - public function getCachedContent() - { - if ($this->_content === null) { - $this->_content = false; - if ($this->cache instanceof Cache) { - $key = $this->calculateKey(); - $data = $this->cache->get($key); - if (is_array($data) && count($data) === 2) { - list ($content, $placeholders) = $data; - if (is_array($placeholders) && count($placeholders) > 0) { - if (empty($this->getView()->cacheStack)) { - // outermost cache: replace placeholder with dynamic content - $content = $this->updateDynamicContent($content, $placeholders); - } - foreach ($placeholders as $name => $statements) { - $this->getView()->addDynamicPlaceholder($name, $statements); - } - } - $this->_content = $content; - } - } - } - return $this->_content; - } + /** + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. + */ + public function getCachedContent() + { + if ($this->_content === null) { + $this->_content = false; + if ($this->cache instanceof Cache) { + $key = $this->calculateKey(); + $data = $this->cache->get($key); + if (is_array($data) && count($data) === 2) { + list ($content, $placeholders) = $data; + if (is_array($placeholders) && count($placeholders) > 0) { + if (empty($this->getView()->cacheStack)) { + // outermost cache: replace placeholder with dynamic content + $content = $this->updateDynamicContent($content, $placeholders); + } + foreach ($placeholders as $name => $statements) { + $this->getView()->addDynamicPlaceholder($name, $statements); + } + } + $this->_content = $content; + } + } + } - protected function updateDynamicContent($content, $placeholders) - { - foreach ($placeholders as $name => $statements) { - $placeholders[$name] = $this->getView()->evaluateDynamicContent($statements); - } - return strtr($content, $placeholders); - } + return $this->_content; + } - /** - * Generates a unique key used for storing the content in cache. - * The key generated depends on both [[id]] and [[variations]]. - * @return mixed a valid cache key - */ - protected function calculateKey() - { - $factors = [__CLASS__, $this->getId()]; - if (is_array($this->variations)) { - foreach ($this->variations as $factor) { - $factors[] = $factor; - } - } - return $factors; - } + protected function updateDynamicContent($content, $placeholders) + { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->getView()->evaluateDynamicContent($statements); + } + + return strtr($content, $placeholders); + } + + /** + * Generates a unique key used for storing the content in cache. + * The key generated depends on both [[id]] and [[variations]]. + * @return mixed a valid cache key + */ + protected function calculateKey() + { + $factors = [__CLASS__, $this->getId()]; + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + + return $factors; + } } diff --git a/framework/widgets/InputWidget.php b/framework/widgets/InputWidget.php index 321ebb0aff5..197b8b00de5 100644 --- a/framework/widgets/InputWidget.php +++ b/framework/widgets/InputWidget.php @@ -25,49 +25,48 @@ */ class InputWidget extends Widget { - /** - * @var Model the data model that this widget is associated with. - */ - public $model; - /** - * @var string the model attribute that this widget is associated with. - */ - public $attribute; - /** - * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. - */ - public $name; - /** - * @var string the input value. - */ - public $value; - /** - * @var array the HTML attributes for the input tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; + /** + * @var Model the data model that this widget is associated with. + */ + public $model; + /** + * @var string the model attribute that this widget is associated with. + */ + public $attribute; + /** + * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. + */ + public $name; + /** + * @var string the input value. + */ + public $value; + /** + * @var array the HTML attributes for the input tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + if (!$this->hasModel() && $this->name === null) { + throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified."); + } + if (!isset($this->options['id'])) { + $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); + } + parent::init(); + } - /** - * Initializes the widget. - * If you override this method, make sure you call the parent implementation first. - */ - public function init() - { - if (!$this->hasModel() && $this->name === null) { - throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified."); - } - if (!isset($this->options['id'])) { - $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); - } - parent::init(); - } - - /** - * @return boolean whether this widget is associated with a data model. - */ - protected function hasModel() - { - return $this->model instanceof Model && $this->attribute !== null; - } + /** + * @return boolean whether this widget is associated with a data model. + */ + protected function hasModel() + { + return $this->model instanceof Model && $this->attribute !== null; + } } diff --git a/framework/widgets/LinkPager.php b/framework/widgets/LinkPager.php index 8c0a34ca149..0f49099f2a0 100644 --- a/framework/widgets/LinkPager.php +++ b/framework/widgets/LinkPager.php @@ -28,197 +28,199 @@ */ class LinkPager extends Widget { - /** - * @var Pagination the pagination object that this pager is associated with. - * You must set this property in order to make LinkPager work. - */ - public $pagination; - /** - * @var array HTML attributes for the pager container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'pagination']; - /** - * @var array HTML attributes for the link in a pager container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $linkOptions = []; - /** - * @var string the CSS class for the "first" page button. - */ - public $firstPageCssClass = 'first'; - /** - * @var string the CSS class for the "last" page button. - */ - public $lastPageCssClass = 'last'; - /** - * @var string the CSS class for the "previous" page button. - */ - public $prevPageCssClass = 'prev'; - /** - * @var string the CSS class for the "next" page button. - */ - public $nextPageCssClass = 'next'; - /** - * @var string the CSS class for the active (currently selected) page button. - */ - public $activePageCssClass = 'active'; - /** - * @var string the CSS class for the disabled page buttons. - */ - public $disabledPageCssClass = 'disabled'; - /** - * @var integer maximum number of page buttons that can be displayed. Defaults to 10. - */ - public $maxButtonCount = 10; - /** - * @var string the label for the "next" page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "next" page button will not be displayed. - */ - public $nextPageLabel = '»'; - /** - * @var string the text label for the previous page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "previous" page button will not be displayed. - */ - public $prevPageLabel = '«'; - /** - * @var string the text label for the "first" page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "first" page button will not be displayed. - */ - public $firstPageLabel; - /** - * @var string the text label for the "last" page button. Note that this will NOT be HTML-encoded. - * If this property is null, the "last" page button will not be displayed. - */ - public $lastPageLabel; - /** - * @var boolean whether to register link tags in the HTML header for prev, next, first and last page. - * Defaults to `false` to avoid conflicts when multiple pagers are used on one page. - * @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2 - * @see registerLinkTags() - */ - public $registerLinkTags = false; - - - /** - * Initializes the pager. - */ - public function init() - { - if ($this->pagination === null) { - throw new InvalidConfigException('The "pagination" property must be set.'); - } - } - - /** - * Executes the widget. - * This overrides the parent implementation by displaying the generated page buttons. - */ - public function run() - { - if ($this->registerLinkTags) { - $this->registerLinkTags(); - } - echo $this->renderPageButtons(); - } - - /** - * Registers relational link tags in the html header for prev, next, first and last page. - * These links are generated using [[yii\data\Pagination::getLinks()]]. - * @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2 - */ - protected function registerLinkTags() - { - $view = $this->getView(); - foreach ($this->pagination->getLinks() as $rel => $href) { - $view->registerLinkTag(['rel' => $rel, 'href' => $href], $rel); - } - } - - /** - * Renders the page buttons. - * @return string the rendering result - */ - protected function renderPageButtons() - { - $buttons = []; - - $pageCount = $this->pagination->getPageCount(); - $currentPage = $this->pagination->getPage(); - - // first page - if ($this->firstPageLabel !== null) { - $buttons[] = $this->renderPageButton($this->firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false); - } - - // prev page - if ($this->prevPageLabel !== null) { - if (($page = $currentPage - 1) < 0) { - $page = 0; - } - $buttons[] = $this->renderPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); - } - - // internal pages - list($beginPage, $endPage) = $this->getPageRange(); - for ($i = $beginPage; $i <= $endPage; ++$i) { - $buttons[] = $this->renderPageButton($i + 1, $i, null, false, $i == $currentPage); - } - - // next page - if ($this->nextPageLabel !== null) { - if (($page = $currentPage + 1) >= $pageCount - 1) { - $page = $pageCount - 1; - } - $buttons[] = $this->renderPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); - } - - // last page - if ($this->lastPageLabel !== null) { - $buttons[] = $this->renderPageButton($this->lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); - } - - return Html::tag('ul', implode("\n", $buttons), $this->options); - } - - /** - * Renders a page button. - * You may override this method to customize the generation of page buttons. - * @param string $label the text label for the button - * @param integer $page the page number - * @param string $class the CSS class for the page button. - * @param boolean $disabled whether this page button is disabled - * @param boolean $active whether this page button is active - * @return string the rendering result - */ - protected function renderPageButton($label, $page, $class, $disabled, $active) - { - $options = ['class' => $class === '' ? null : $class]; - if ($active) { - Html::addCssClass($options, $this->activePageCssClass); - } - if ($disabled) { - Html::addCssClass($options, $this->disabledPageCssClass); - return Html::tag('li', Html::tag('span', $label), $options); - } - $linkOptions = $this->linkOptions; - $linkOptions['data-page'] = $page; - return Html::tag('li', Html::a($label, $this->pagination->createUrl($page), $linkOptions), $options); - } - - /** - * @return array the begin and end pages that need to be displayed. - */ - protected function getPageRange() - { - $currentPage = $this->pagination->getPage(); - $pageCount = $this->pagination->getPageCount(); - - $beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2)); - if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { - $endPage = $pageCount - 1; - $beginPage = max(0, $endPage - $this->maxButtonCount + 1); - } - return [$beginPage, $endPage]; - } + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make LinkPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the pager container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'pagination']; + /** + * @var array HTML attributes for the link in a pager container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $linkOptions = []; + /** + * @var string the CSS class for the "first" page button. + */ + public $firstPageCssClass = 'first'; + /** + * @var string the CSS class for the "last" page button. + */ + public $lastPageCssClass = 'last'; + /** + * @var string the CSS class for the "previous" page button. + */ + public $prevPageCssClass = 'prev'; + /** + * @var string the CSS class for the "next" page button. + */ + public $nextPageCssClass = 'next'; + /** + * @var string the CSS class for the active (currently selected) page button. + */ + public $activePageCssClass = 'active'; + /** + * @var string the CSS class for the disabled page buttons. + */ + public $disabledPageCssClass = 'disabled'; + /** + * @var integer maximum number of page buttons that can be displayed. Defaults to 10. + */ + public $maxButtonCount = 10; + /** + * @var string the label for the "next" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "next" page button will not be displayed. + */ + public $nextPageLabel = '»'; + /** + * @var string the text label for the previous page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "previous" page button will not be displayed. + */ + public $prevPageLabel = '«'; + /** + * @var string the text label for the "first" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "first" page button will not be displayed. + */ + public $firstPageLabel; + /** + * @var string the text label for the "last" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "last" page button will not be displayed. + */ + public $lastPageLabel; + /** + * @var boolean whether to register link tags in the HTML header for prev, next, first and last page. + * Defaults to `false` to avoid conflicts when multiple pagers are used on one page. + * @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2 + * @see registerLinkTags() + */ + public $registerLinkTags = false; + + /** + * Initializes the pager. + */ + public function init() + { + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + */ + public function run() + { + if ($this->registerLinkTags) { + $this->registerLinkTags(); + } + echo $this->renderPageButtons(); + } + + /** + * Registers relational link tags in the html header for prev, next, first and last page. + * These links are generated using [[yii\data\Pagination::getLinks()]]. + * @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2 + */ + protected function registerLinkTags() + { + $view = $this->getView(); + foreach ($this->pagination->getLinks() as $rel => $href) { + $view->registerLinkTag(['rel' => $rel, 'href' => $href], $rel); + } + } + + /** + * Renders the page buttons. + * @return string the rendering result + */ + protected function renderPageButtons() + { + $buttons = []; + + $pageCount = $this->pagination->getPageCount(); + $currentPage = $this->pagination->getPage(); + + // first page + if ($this->firstPageLabel !== null) { + $buttons[] = $this->renderPageButton($this->firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false); + } + + // prev page + if ($this->prevPageLabel !== null) { + if (($page = $currentPage - 1) < 0) { + $page = 0; + } + $buttons[] = $this->renderPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); + } + + // internal pages + list($beginPage, $endPage) = $this->getPageRange(); + for ($i = $beginPage; $i <= $endPage; ++$i) { + $buttons[] = $this->renderPageButton($i + 1, $i, null, false, $i == $currentPage); + } + + // next page + if ($this->nextPageLabel !== null) { + if (($page = $currentPage + 1) >= $pageCount - 1) { + $page = $pageCount - 1; + } + $buttons[] = $this->renderPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); + } + + // last page + if ($this->lastPageLabel !== null) { + $buttons[] = $this->renderPageButton($this->lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); + } + + return Html::tag('ul', implode("\n", $buttons), $this->options); + } + + /** + * Renders a page button. + * You may override this method to customize the generation of page buttons. + * @param string $label the text label for the button + * @param integer $page the page number + * @param string $class the CSS class for the page button. + * @param boolean $disabled whether this page button is disabled + * @param boolean $active whether this page button is active + * @return string the rendering result + */ + protected function renderPageButton($label, $page, $class, $disabled, $active) + { + $options = ['class' => $class === '' ? null : $class]; + if ($active) { + Html::addCssClass($options, $this->activePageCssClass); + } + if ($disabled) { + Html::addCssClass($options, $this->disabledPageCssClass); + + return Html::tag('li', Html::tag('span', $label), $options); + } + $linkOptions = $this->linkOptions; + $linkOptions['data-page'] = $page; + + return Html::tag('li', Html::a($label, $this->pagination->createUrl($page), $linkOptions), $options); + } + + /** + * @return array the begin and end pages that need to be displayed. + */ + protected function getPageRange() + { + $currentPage = $this->pagination->getPage(); + $pageCount = $this->pagination->getPageCount(); + + $beginPage = max(0, $currentPage - (int) ($this->maxButtonCount / 2)); + if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { + $endPage = $pageCount - 1; + $beginPage = max(0, $endPage - $this->maxButtonCount + 1); + } + + return [$beginPage, $endPage]; + } } diff --git a/framework/widgets/LinkSorter.php b/framework/widgets/LinkSorter.php index 3dc9e6f53ec..30706dc88a7 100644 --- a/framework/widgets/LinkSorter.php +++ b/framework/widgets/LinkSorter.php @@ -23,53 +23,53 @@ */ class LinkSorter extends Widget { - /** - * @var Sort the sort definition - */ - public $sort; - /** - * @var array list of the attributes that support sorting. If not set, it will be determined - * using [[Sort::attributes]]. - */ - public $attributes; - /** - * @var array HTML attributes for the sorter container tag. - * @see \yii\helpers\Html::ul() for special attributes. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'sorter']; + /** + * @var Sort the sort definition + */ + public $sort; + /** + * @var array list of the attributes that support sorting. If not set, it will be determined + * using [[Sort::attributes]]. + */ + public $attributes; + /** + * @var array HTML attributes for the sorter container tag. + * @see \yii\helpers\Html::ul() for special attributes. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'sorter']; + /** + * Initializes the sorter. + */ + public function init() + { + if ($this->sort === null) { + throw new InvalidConfigException('The "sort" property must be set.'); + } + } - /** - * Initializes the sorter. - */ - public function init() - { - if ($this->sort === null) { - throw new InvalidConfigException('The "sort" property must be set.'); - } - } + /** + * Executes the widget. + * This method renders the sort links. + */ + public function run() + { + echo $this->renderSortLinks(); + } - /** - * Executes the widget. - * This method renders the sort links. - */ - public function run() - { - echo $this->renderSortLinks(); - } + /** + * Renders the sort links. + * @return string the rendering result + */ + protected function renderSortLinks() + { + $attributes = empty($this->attributes) ? array_keys($this->sort->attributes) : $this->attributes; + $links = []; + foreach ($attributes as $name) { + $links[] = $this->sort->link($name); + } - /** - * Renders the sort links. - * @return string the rendering result - */ - protected function renderSortLinks() - { - $attributes = empty($this->attributes) ? array_keys($this->sort->attributes) : $this->attributes; - $links = []; - foreach ($attributes as $name) { - $links[] = $this->sort->link($name); - } - return Html::ul($links, array_merge($this->options, ['encode' => false])); - } + return Html::ul($links, array_merge($this->options, ['encode' => false])); + } } diff --git a/framework/widgets/ListView.php b/framework/widgets/ListView.php index 66690bb2cfe..0825427edbd 100644 --- a/framework/widgets/ListView.php +++ b/framework/widgets/ListView.php @@ -18,92 +18,93 @@ */ class ListView extends BaseListView { - /** - * @var array the HTML attributes for the container of the rendering result of each data model. - * The "tag" element specifies the tag name of the container element and defaults to "div". - * If "tag" is false, it means no container element will be rendered. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; - /** - * @var string|callable the name of the view for rendering each data item, or a callback (e.g. an anonymous function) - * for rendering each data item. If it specifies a view name, the following variables will - * be available in the view: - * - * - `$model`: mixed, the data model - * - `$key`: mixed, the key value associated with the data item - * - `$index`: integer, the zero-based index of the data item in the items array returned by [[dataProvider]]. - * - `$widget`: ListView, this widget instance - * - * Note that the view name is resolved into the view file by the current context of the [[view]] object. - * - * If this property is specified as a callback, it should have the following signature: - * - * ~~~ - * function ($model, $key, $index, $widget) - * ~~~ - */ - public $itemView; - /** - * @var array additional parameters to be passed to [[itemView]] when it is being rendered. - * This property is used only when [[itemView]] is a string representing a view name. - */ - public $viewParams = []; - /** - * @var string the HTML code to be displayed between any two consecutive items. - */ - public $separator = "\n"; - /** - * @var array the HTML attributes for the container tag of the list view. - * The "tag" element specifies the tag name of the container element and defaults to "div". - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'list-view']; + /** + * @var array the HTML attributes for the container of the rendering result of each data model. + * The "tag" element specifies the tag name of the container element and defaults to "div". + * If "tag" is false, it means no container element will be rendered. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * @var string|callable the name of the view for rendering each data item, or a callback (e.g. an anonymous function) + * for rendering each data item. If it specifies a view name, the following variables will + * be available in the view: + * + * - `$model`: mixed, the data model + * - `$key`: mixed, the key value associated with the data item + * - `$index`: integer, the zero-based index of the data item in the items array returned by [[dataProvider]]. + * - `$widget`: ListView, this widget instance + * + * Note that the view name is resolved into the view file by the current context of the [[view]] object. + * + * If this property is specified as a callback, it should have the following signature: + * + * ~~~ + * function ($model, $key, $index, $widget) + * ~~~ + */ + public $itemView; + /** + * @var array additional parameters to be passed to [[itemView]] when it is being rendered. + * This property is used only when [[itemView]] is a string representing a view name. + */ + public $viewParams = []; + /** + * @var string the HTML code to be displayed between any two consecutive items. + */ + public $separator = "\n"; + /** + * @var array the HTML attributes for the container tag of the list view. + * The "tag" element specifies the tag name of the container element and defaults to "div". + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'list-view']; + /** + * Renders all data models. + * @return string the rendering result + */ + public function renderItems() + { + $models = $this->dataProvider->getModels(); + $keys = $this->dataProvider->getKeys(); + $rows = []; + foreach (array_values($models) as $index => $model) { + $rows[] = $this->renderItem($model, $keys[$index], $index); + } - /** - * Renders all data models. - * @return string the rendering result - */ - public function renderItems() - { - $models = $this->dataProvider->getModels(); - $keys = $this->dataProvider->getKeys(); - $rows = []; - foreach (array_values($models) as $index => $model) { - $rows[] = $this->renderItem($model, $keys[$index], $index); - } - return implode($this->separator, $rows); - } + return implode($this->separator, $rows); + } - /** - * Renders a single data model. - * @param mixed $model the data model to be rendered - * @param mixed $key the key value associated with the data model - * @param integer $index the zero-based index of the data model in the model array returned by [[dataProvider]]. - * @return string the rendering result - */ - public function renderItem($model, $key, $index) - { - if ($this->itemView === null) { - $content = $key; - } elseif (is_string($this->itemView)) { - $content = $this->getView()->render($this->itemView, array_merge([ - 'model' => $model, - 'key' => $key, - 'index' => $index, - 'widget' => $this, - ], $this->viewParams)); - } else { - $content = call_user_func($this->itemView, $model, $key, $index, $this); - } - $options = $this->itemOptions; - $tag = ArrayHelper::remove($options, 'tag', 'div'); - if ($tag !== false) { - $options['data-key'] = is_array($key) ? json_encode($key) : (string)$key; - return Html::tag($tag, $content, $options); - } else { - return $content; - } - } + /** + * Renders a single data model. + * @param mixed $model the data model to be rendered + * @param mixed $key the key value associated with the data model + * @param integer $index the zero-based index of the data model in the model array returned by [[dataProvider]]. + * @return string the rendering result + */ + public function renderItem($model, $key, $index) + { + if ($this->itemView === null) { + $content = $key; + } elseif (is_string($this->itemView)) { + $content = $this->getView()->render($this->itemView, array_merge([ + 'model' => $model, + 'key' => $key, + 'index' => $index, + 'widget' => $this, + ], $this->viewParams)); + } else { + $content = call_user_func($this->itemView, $model, $key, $index, $this); + } + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + if ($tag !== false) { + $options['data-key'] = is_array($key) ? json_encode($key) : (string) $key; + + return Html::tag($tag, $content, $options); + } else { + return $content; + } + } } diff --git a/framework/widgets/MaskedInput.php b/framework/widgets/MaskedInput.php index a060b1a6737..6ae22635cb0 100644 --- a/framework/widgets/MaskedInput.php +++ b/framework/widgets/MaskedInput.php @@ -36,99 +36,98 @@ */ class MaskedInput extends InputWidget { - /** - * @var string the input mask (e.g. '99/99/9999' for date input). The following characters are predefined: - * - * - `a`: represents an alpha character (A-Z, a-z) - * - `9`: represents a numeric character (0-9) - * - `*`: represents an alphanumeric character (A-Z, a-z, 0-9) - * - `?`: anything listed after '?' within the mask is considered optional user input - * - * Additional characters can be defined by specifying the [[charMap]] property. - */ - public $mask; - /** - * @var array the mapping between mask characters and the corresponding patterns. - * For example, `['~' => '[+-]']` specifies that the '~' character expects '+' or '-' input. - * Defaults to null, meaning using the map as described in [[mask]]. - */ - public $charMap; - /** - * @var string the character prompting for user input. Defaults to underscore '_'. - */ - public $placeholder; - /** - * @var string a JavaScript function callback that will be invoked when user finishes the input. - */ - public $completed; - /** - * @var array the HTML attributes for the input tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = ['class' => 'form-control']; + /** + * @var string the input mask (e.g. '99/99/9999' for date input). The following characters are predefined: + * + * - `a`: represents an alpha character (A-Z, a-z) + * - `9`: represents a numeric character (0-9) + * - `*`: represents an alphanumeric character (A-Z, a-z, 0-9) + * - `?`: anything listed after '?' within the mask is considered optional user input + * + * Additional characters can be defined by specifying the [[charMap]] property. + */ + public $mask; + /** + * @var array the mapping between mask characters and the corresponding patterns. + * For example, `['~' => '[+-]']` specifies that the '~' character expects '+' or '-' input. + * Defaults to null, meaning using the map as described in [[mask]]. + */ + public $charMap; + /** + * @var string the character prompting for user input. Defaults to underscore '_'. + */ + public $placeholder; + /** + * @var string a JavaScript function callback that will be invoked when user finishes the input. + */ + public $completed; + /** + * @var array the HTML attributes for the input tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = ['class' => 'form-control']; + /** + * Initializes the widget. + * @throws InvalidConfigException if the "mask" property is not set. + */ + public function init() + { + parent::init(); + if (empty($this->mask)) { + throw new InvalidConfigException('The "mask" property must be set.'); + } + } - /** - * Initializes the widget. - * @throws InvalidConfigException if the "mask" property is not set. - */ - public function init() - { - parent::init(); - if (empty($this->mask)) { - throw new InvalidConfigException('The "mask" property must be set.'); - } - } + /** + * Runs the widget. + */ + public function run() + { + if ($this->hasModel()) { + echo Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + echo Html::textInput($this->name, $this->value, $this->options); + } + $this->registerClientScript(); + } - /** - * Runs the widget. - */ - public function run() - { - if ($this->hasModel()) { - echo Html::activeTextInput($this->model, $this->attribute, $this->options); - } else { - echo Html::textInput($this->name, $this->value, $this->options); - } - $this->registerClientScript(); - } + /** + * Registers the needed JavaScript. + */ + public function registerClientScript() + { + $options = $this->getClientOptions(); + $options = empty($options) ? '' : ',' . Json::encode($options); + $js = ''; + if (is_array($this->charMap) && !empty($this->charMap)) { + $js .= 'jQuery.mask.definitions=' . Json::encode($this->charMap) . ";\n"; + } + $id = $this->options['id']; + $js .= "jQuery(\"#{$id}\").mask(\"{$this->mask}\"{$options});"; + $view = $this->getView(); + MaskedInputAsset::register($view); + $view->registerJs($js); + } - /** - * Registers the needed JavaScript. - */ - public function registerClientScript() - { - $options = $this->getClientOptions(); - $options = empty($options) ? '' : ',' . Json::encode($options); - $js = ''; - if (is_array($this->charMap) && !empty($this->charMap)) { - $js .= 'jQuery.mask.definitions=' . Json::encode($this->charMap) . ";\n"; - } - $id = $this->options['id']; - $js .= "jQuery(\"#{$id}\").mask(\"{$this->mask}\"{$options});"; - $view = $this->getView(); - MaskedInputAsset::register($view); - $view->registerJs($js); - } + /** + * @return array the options for the text field + */ + protected function getClientOptions() + { + $options = []; + if ($this->placeholder !== null) { + $options['placeholder'] = $this->placeholder; + } - /** - * @return array the options for the text field - */ - protected function getClientOptions() - { - $options = []; - if ($this->placeholder !== null) { - $options['placeholder'] = $this->placeholder; - } + if ($this->completed !== null) { + if ($this->completed instanceof JsExpression) { + $options['completed'] = $this->completed; + } else { + $options['completed'] = new JsExpression($this->completed); + } + } - if ($this->completed !== null) { - if ($this->completed instanceof JsExpression) { - $options['completed'] = $this->completed; - } else { - $options['completed'] = new JsExpression($this->completed); - } - } - - return $options; - } + return $options; + } } diff --git a/framework/widgets/MaskedInputAsset.php b/framework/widgets/MaskedInputAsset.php index 475cf34161c..90e85e75762 100644 --- a/framework/widgets/MaskedInputAsset.php +++ b/framework/widgets/MaskedInputAsset.php @@ -15,11 +15,11 @@ */ class MaskedInputAsset extends AssetBundle { - public $sourcePath = '@yii/assets'; - public $js = [ - 'jquery.maskedinput.js', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@yii/assets'; + public $js = [ + 'jquery.maskedinput.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/framework/widgets/Menu.php b/framework/widgets/Menu.php index eb0bc4ac33c..3678a1d30cf 100644 --- a/framework/widgets/Menu.php +++ b/framework/widgets/Menu.php @@ -48,265 +48,271 @@ */ class Menu extends Widget { - /** - * @var array list of menu items. Each menu item should be an array of the following structure: - * - * - label: string, optional, specifies the menu item label. When [[encodeLabels]] is true, the label - * will be HTML-encoded. If the label is not specified, an empty string will be used. - * - url: string or array, optional, specifies the URL of the menu item. It will be processed by [[Url::to]]. - * When this is set, the actual menu item content will be generated using [[linkTemplate]]; - * otherwise, [[labelTemplate]] will be used. - * - visible: boolean, optional, whether this menu item is visible. Defaults to true. - * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items. - * - active: boolean, optional, whether this menu item is in active state (currently selected). - * If a menu item is active, its CSS class will be appended with [[activeCssClass]]. - * If this option is not set, the menu item will be set active automatically when the current request - * is triggered by [[url]]. For more details, please refer to [[isItemActive()]]. - * - template: string, optional, the template used to render the content of this menu item. - * The token `{url}` will be replaced by the URL associated with this menu item, - * and the token `{label}` will be replaced by the label of the menu item. - * If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead. - * - options: array, optional, the HTML attributes for the menu container tag. - */ - public $items = []; - /** - * @var array list of HTML attributes for the menu container tag. This will be overwritten - * by the "options" set in individual [[items]]. The following special options are recognized: - * - * - tag: string, defaults to "li", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $itemOptions = []; - /** - * @var string the template used to render the body of a menu which is a link. - * In this template, the token `{url}` will be replaced with the corresponding link URL; - * while `{label}` will be replaced with the link text. - * This property will be overridden by the `template` option set in individual menu items via [[items]]. - */ - public $linkTemplate = '{label}'; - /** - * @var string the template used to render the body of a menu which is NOT a link. - * In this template, the token `{label}` will be replaced with the label of the menu item. - * This property will be overridden by the `template` option set in individual menu items via [[items]]. - */ - public $labelTemplate = '{label}'; - /** - * @var string the template used to render a list of sub-menus. - * In this template, the token `{items}` will be replaced with the renderer sub-menu items. - */ - public $submenuTemplate = "\n
          \n{items}\n
        \n"; - /** - * @var boolean whether the labels for menu items should be HTML-encoded. - */ - public $encodeLabels = true; - /** - * @var string the CSS class to be appended to the active menu item. - */ - public $activeCssClass = 'active'; - /** - * @var boolean whether to automatically activate items according to whether their route setting - * matches the currently requested route. - * @see isItemActive() - */ - public $activateItems = true; - /** - * @var boolean whether to activate parent menu items when one of the corresponding child menu items is active. - * The activated parent menu items will also have its CSS classes appended with [[activeCssClass]]. - */ - public $activateParents = false; - /** - * @var boolean whether to hide empty menu items. An empty menu item is one whose `url` option is not - * set and which has no visible child menu items. - */ - public $hideEmptyItems = true; - /** - * @var array the HTML attributes for the menu's container tag. The following special options are recognized: - * - * - tag: string, defaults to "ul", the tag name of the item container tags. - * - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. - * Defaults to null, meaning no such CSS class will be assigned. - */ - public $firstItemCssClass; - /** - * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. - * Defaults to null, meaning no such CSS class will be assigned. - */ - public $lastItemCssClass; - /** - * @var string the route used to determine if a menu item is active or not. - * If not set, it will use the route of the current request. - * @see params - * @see isItemActive() - */ - public $route; - /** - * @var array the parameters used to determine if a menu item is active or not. - * If not set, it will use `$_GET`. - * @see route - * @see isItemActive() - */ - public $params; + /** + * @var array list of menu items. Each menu item should be an array of the following structure: + * + * - label: string, optional, specifies the menu item label. When [[encodeLabels]] is true, the label + * will be HTML-encoded. If the label is not specified, an empty string will be used. + * - url: string or array, optional, specifies the URL of the menu item. It will be processed by [[Url::to]]. + * When this is set, the actual menu item content will be generated using [[linkTemplate]]; + * otherwise, [[labelTemplate]] will be used. + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items. + * - active: boolean, optional, whether this menu item is in active state (currently selected). + * If a menu item is active, its CSS class will be appended with [[activeCssClass]]. + * If this option is not set, the menu item will be set active automatically when the current request + * is triggered by [[url]]. For more details, please refer to [[isItemActive()]]. + * - template: string, optional, the template used to render the content of this menu item. + * The token `{url}` will be replaced by the URL associated with this menu item, + * and the token `{label}` will be replaced by the label of the menu item. + * If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead. + * - options: array, optional, the HTML attributes for the menu container tag. + */ + public $items = []; + /** + * @var array list of HTML attributes for the menu container tag. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * @var string the template used to render the body of a menu which is a link. + * In this template, the token `{url}` will be replaced with the corresponding link URL; + * while `{label}` will be replaced with the link text. + * This property will be overridden by the `template` option set in individual menu items via [[items]]. + */ + public $linkTemplate = '{label}'; + /** + * @var string the template used to render the body of a menu which is NOT a link. + * In this template, the token `{label}` will be replaced with the label of the menu item. + * This property will be overridden by the `template` option set in individual menu items via [[items]]. + */ + public $labelTemplate = '{label}'; + /** + * @var string the template used to render a list of sub-menus. + * In this template, the token `{items}` will be replaced with the renderer sub-menu items. + */ + public $submenuTemplate = "\n
          \n{items}\n
        \n"; + /** + * @var boolean whether the labels for menu items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var string the CSS class to be appended to the active menu item. + */ + public $activeCssClass = 'active'; + /** + * @var boolean whether to automatically activate items according to whether their route setting + * matches the currently requested route. + * @see isItemActive() + */ + public $activateItems = true; + /** + * @var boolean whether to activate parent menu items when one of the corresponding child menu items is active. + * The activated parent menu items will also have its CSS classes appended with [[activeCssClass]]. + */ + public $activateParents = false; + /** + * @var boolean whether to hide empty menu items. An empty menu item is one whose `url` option is not + * set and which has no visible child menu items. + */ + public $hideEmptyItems = true; + /** + * @var array the HTML attributes for the menu's container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + */ + public $firstItemCssClass; + /** + * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + */ + public $lastItemCssClass; + /** + * @var string the route used to determine if a menu item is active or not. + * If not set, it will use the route of the current request. + * @see params + * @see isItemActive() + */ + public $route; + /** + * @var array the parameters used to determine if a menu item is active or not. + * If not set, it will use `$_GET`. + * @see route + * @see isItemActive() + */ + public $params; - /** - * Renders the menu. - */ - public function run() - { - if ($this->route === null && Yii::$app->controller !== null) { - $this->route = Yii::$app->controller->getRoute(); - } - if ($this->params === null) { - $this->params = Yii::$app->request->getQueryParams(); - } - $items = $this->normalizeItems($this->items, $hasActiveChild); - $options = $this->options; - $tag = ArrayHelper::remove($options, 'tag', 'ul'); - echo Html::tag($tag, $this->renderItems($items), $options); - } + /** + * Renders the menu. + */ + public function run() + { + if ($this->route === null && Yii::$app->controller !== null) { + $this->route = Yii::$app->controller->getRoute(); + } + if ($this->params === null) { + $this->params = Yii::$app->request->getQueryParams(); + } + $items = $this->normalizeItems($this->items, $hasActiveChild); + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::tag($tag, $this->renderItems($items), $options); + } - /** - * Recursively renders the menu items (without the container tag). - * @param array $items the menu items to be rendered recursively - * @return string the rendering result - */ - protected function renderItems($items) - { - $n = count($items); - $lines = []; - foreach ($items as $i => $item) { - $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); - $tag = ArrayHelper::remove($options, 'tag', 'li'); - $class = []; - if ($item['active']) { - $class[] = $this->activeCssClass; - } - if ($i === 0 && $this->firstItemCssClass !== null) { - $class[] = $this->firstItemCssClass; - } - if ($i === $n - 1 && $this->lastItemCssClass !== null) { - $class[] = $this->lastItemCssClass; - } - if (!empty($class)) { - if (empty($options['class'])) { - $options['class'] = implode(' ', $class); - } else { - $options['class'] .= ' ' . implode(' ', $class); - } - } + /** + * Recursively renders the menu items (without the container tag). + * @param array $items the menu items to be rendered recursively + * @return string the rendering result + */ + protected function renderItems($items) + { + $n = count($items); + $lines = []; + foreach ($items as $i => $item) { + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', 'li'); + $class = []; + if ($item['active']) { + $class[] = $this->activeCssClass; + } + if ($i === 0 && $this->firstItemCssClass !== null) { + $class[] = $this->firstItemCssClass; + } + if ($i === $n - 1 && $this->lastItemCssClass !== null) { + $class[] = $this->lastItemCssClass; + } + if (!empty($class)) { + if (empty($options['class'])) { + $options['class'] = implode(' ', $class); + } else { + $options['class'] .= ' ' . implode(' ', $class); + } + } - $menu = $this->renderItem($item); - if (!empty($item['items'])) { - $menu .= strtr($this->submenuTemplate, [ - '{items}' => $this->renderItems($item['items']), - ]); - } - $lines[] = Html::tag($tag, $menu, $options); - } - return implode("\n", $lines); - } + $menu = $this->renderItem($item); + if (!empty($item['items'])) { + $menu .= strtr($this->submenuTemplate, [ + '{items}' => $this->renderItems($item['items']), + ]); + } + $lines[] = Html::tag($tag, $menu, $options); + } - /** - * Renders the content of a menu item. - * Note that the container and the sub-menus are not rendered here. - * @param array $item the menu item to be rendered. Please refer to [[items]] to see what data might be in the item. - * @return string the rendering result - */ - protected function renderItem($item) - { - if (isset($item['url'])) { - $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); - return strtr($template, [ - '{url}' => Url::to($item['url']), - '{label}' => $item['label'], - ]); - } else { - $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate); - return strtr($template, [ - '{label}' => $item['label'], - ]); - } - } + return implode("\n", $lines); + } - /** - * Normalizes the [[items]] property to remove invisible items and activate certain items. - * @param array $items the items to be normalized. - * @param boolean $active whether there is an active child menu item. - * @return array the normalized menu items - */ - protected function normalizeItems($items, &$active) - { - foreach ($items as $i => $item) { - if (isset($item['visible']) && !$item['visible']) { - unset($items[$i]); - continue; - } - if (!isset($item['label'])) { - $item['label'] = ''; - } - if ($this->encodeLabels) { - $items[$i]['label'] = Html::encode($item['label']); - } - $hasActiveChild = false; - if (isset($item['items'])) { - $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild); - if (empty($items[$i]['items']) && $this->hideEmptyItems) { - unset($items[$i]['items']); - if (!isset($item['url'])) { - unset($items[$i]); - continue; - } - } - } - if (!isset($item['active'])) { - if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) { - $active = $items[$i]['active'] = true; - } else { - $items[$i]['active'] = false; - } - } elseif ($item['active']) { - $active = true; - } - } - return array_values($items); - } + /** + * Renders the content of a menu item. + * Note that the container and the sub-menus are not rendered here. + * @param array $item the menu item to be rendered. Please refer to [[items]] to see what data might be in the item. + * @return string the rendering result + */ + protected function renderItem($item) + { + if (isset($item['url'])) { + $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); - /** - * Checks whether a menu item is active. - * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. - * When the `url` option of a menu item is specified in terms of an array, its first element is treated - * as the route for the item and the rest of the elements are the associated parameters. - * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item - * be considered active. - * @param array $item the menu item to be checked - * @return boolean whether the menu item is active - */ - protected function isItemActive($item) - { - if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { - $route = $item['url'][0]; - if ($route[0] !== '/' && Yii::$app->controller) { - $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; - } - if (ltrim($route, '/') !== $this->route) { - return false; - } - unset($item['url']['#']); - if (count($item['url']) > 1) { - foreach (array_splice($item['url'], 1) as $name => $value) { - if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) { - return false; - } - } - } - return true; - } - return false; - } + return strtr($template, [ + '{url}' => Url::to($item['url']), + '{label}' => $item['label'], + ]); + } else { + $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate); + + return strtr($template, [ + '{label}' => $item['label'], + ]); + } + } + + /** + * Normalizes the [[items]] property to remove invisible items and activate certain items. + * @param array $items the items to be normalized. + * @param boolean $active whether there is an active child menu item. + * @return array the normalized menu items + */ + protected function normalizeItems($items, &$active) + { + foreach ($items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + if (!isset($item['label'])) { + $item['label'] = ''; + } + if ($this->encodeLabels) { + $items[$i]['label'] = Html::encode($item['label']); + } + $hasActiveChild = false; + if (isset($item['items'])) { + $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild); + if (empty($items[$i]['items']) && $this->hideEmptyItems) { + unset($items[$i]['items']); + if (!isset($item['url'])) { + unset($items[$i]); + continue; + } + } + } + if (!isset($item['active'])) { + if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) { + $active = $items[$i]['active'] = true; + } else { + $items[$i]['active'] = false; + } + } elseif ($item['active']) { + $active = true; + } + } + + return array_values($items); + } + + /** + * Checks whether a menu item is active. + * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. + * When the `url` option of a menu item is specified in terms of an array, its first element is treated + * as the route for the item and the rest of the elements are the associated parameters. + * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item + * be considered active. + * @param array $item the menu item to be checked + * @return boolean whether the menu item is active + */ + protected function isItemActive($item) + { + if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { + $route = $item['url'][0]; + if ($route[0] !== '/' && Yii::$app->controller) { + $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; + } + if (ltrim($route, '/') !== $this->route) { + return false; + } + unset($item['url']['#']); + if (count($item['url']) > 1) { + foreach (array_splice($item['url'], 1) as $name => $value) { + if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) { + return false; + } + } + } + + return true; + } + + return false; + } } diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 0833f66103d..e5eaab82a6d 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -43,138 +43,140 @@ */ class Pjax extends Widget { - /** - * @var array the HTML attributes for the widget container tag. - * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. - */ - public $options = []; - /** - * @var string the jQuery selector of the links that should trigger pjax requests. - * If not set, all links within the enclosed content of Pjax will trigger pjax requests. - * Note that if the response to the pjax request is a full page, a normal request will be sent again. - */ - public $linkSelector; - /** - * @var string the jQuery selector of the forms whose submissions should trigger pjax requests. - * If not set, all forms with `data-pjax` attribute within the enclosed content of Pjax will trigger pjax requests. - * Note that if the response to the pjax request is a full page, a normal request will be sent again. - */ - public $formSelector; - /** - * @var boolean whether to enable push state. - */ - public $enablePushState = true; - /** - * @var boolean whether to enable replace state. - */ - public $enableReplaceState = false; - /** - * @var integer pjax timeout setting (in milliseconds). This timeout is used when making AJAX requests. - * Use a bigger number if your server is slow. If the server does not respond within the timeout, - * a full page load will be triggered. - */ - public $timeout = 1000; - /** - * @var boolean|integer how to scroll the page when pjax response is received. If false, no page scroll will be made. - * Use a number if you want to scroll to a particular place. - */ - public $scrollTo = false; - /** - * @var array additional options to be passed to the pjax JS plugin. Please refer to the - * [pjax project page](https://github.com/yiisoft/jquery-pjax) for available options. - */ - public $clientOptions; - - /** - * @inheritdoc - */ - public function init() - { - if (!isset($this->options['id'])) { - $this->options['id'] = $this->getId(); - } - - if ($this->requiresPjax()) { - ob_start(); - ob_implicit_flush(false); - $view = $this->getView(); - $view->clear(); - $view->beginPage(); - $view->head(); - $view->beginBody(); - if ($view->title !== null) { - echo Html::tag('title', Html::encode($view->title)); - } - } else { - echo Html::beginTag('div', $this->options); - } - } - - /** - * @inheritdoc - */ - public function run() - { - if (!$this->requiresPjax()) { - echo Html::endTag('div'); - $this->registerClientScript(); - return; - } - - $view = $this->getView(); - $view->endBody(); - - // Do not re-send css files as it may override the css files that were loaded after them. - // This is a temporary fix for https://github.com/yiisoft/yii2/issues/2310 - // It should be removed once pjax supports loading only missing css files - $view->cssFiles = null; - - $view->endPage(true); - - $content = ob_get_clean(); - - // only need the content enclosed within this widget - $response = Yii::$app->getResponse(); - $level = ob_get_level(); - $response->clearOutputBuffers(); - $response->setStatusCode(200); - $response->format = Response::FORMAT_HTML; - $response->content = $content; - $response->send(); - - // re-enable output buffer to capture content after this widget - for (; $level > 0; --$level) { - ob_start(); - ob_implicit_flush(false); - } - } - - /** - * @return boolean whether the current request requires pjax response from this widget - */ - protected function requiresPjax() - { - $headers = Yii::$app->getRequest()->getHeaders(); - return $headers->get('X-Pjax') && $headers->get('X-Pjax-Container') === '#' . $this->getId(); - } - - /** - * Registers the needed JavaScript. - */ - public function registerClientScript() - { - $id = $this->options['id']; - $this->clientOptions['push'] = $this->enablePushState; - $this->clientOptions['replace'] = $this->enableReplaceState; - $this->clientOptions['timeout'] = $this->timeout; - $this->clientOptions['scrollTo'] = $this->scrollTo; - $options = Json::encode($this->clientOptions); - $linkSelector = Json::encode($this->linkSelector !== null ? $this->linkSelector : '#' . $id . ' a'); - $formSelector = Json::encode($this->formSelector !== null ? $this->formSelector : '#' . $id . ' form[data-pjax]'); - $view = $this->getView(); - PjaxAsset::register($view); - $js = "jQuery(document).pjax($linkSelector, \"#$id\", $options);"; - $js .= "\njQuery(document).on('submit', $formSelector, function (event) {jQuery.pjax.submit(event, '#$id', $options);});"; - $view->registerJs($js); - } + /** + * @var array the HTML attributes for the widget container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var string the jQuery selector of the links that should trigger pjax requests. + * If not set, all links within the enclosed content of Pjax will trigger pjax requests. + * Note that if the response to the pjax request is a full page, a normal request will be sent again. + */ + public $linkSelector; + /** + * @var string the jQuery selector of the forms whose submissions should trigger pjax requests. + * If not set, all forms with `data-pjax` attribute within the enclosed content of Pjax will trigger pjax requests. + * Note that if the response to the pjax request is a full page, a normal request will be sent again. + */ + public $formSelector; + /** + * @var boolean whether to enable push state. + */ + public $enablePushState = true; + /** + * @var boolean whether to enable replace state. + */ + public $enableReplaceState = false; + /** + * @var integer pjax timeout setting (in milliseconds). This timeout is used when making AJAX requests. + * Use a bigger number if your server is slow. If the server does not respond within the timeout, + * a full page load will be triggered. + */ + public $timeout = 1000; + /** + * @var boolean|integer how to scroll the page when pjax response is received. If false, no page scroll will be made. + * Use a number if you want to scroll to a particular place. + */ + public $scrollTo = false; + /** + * @var array additional options to be passed to the pjax JS plugin. Please refer to the + * [pjax project page](https://github.com/yiisoft/jquery-pjax) for available options. + */ + public $clientOptions; + + /** + * @inheritdoc + */ + public function init() + { + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + + if ($this->requiresPjax()) { + ob_start(); + ob_implicit_flush(false); + $view = $this->getView(); + $view->clear(); + $view->beginPage(); + $view->head(); + $view->beginBody(); + if ($view->title !== null) { + echo Html::tag('title', Html::encode($view->title)); + } + } else { + echo Html::beginTag('div', $this->options); + } + } + + /** + * @inheritdoc + */ + public function run() + { + if (!$this->requiresPjax()) { + echo Html::endTag('div'); + $this->registerClientScript(); + + return; + } + + $view = $this->getView(); + $view->endBody(); + + // Do not re-send css files as it may override the css files that were loaded after them. + // This is a temporary fix for https://github.com/yiisoft/yii2/issues/2310 + // It should be removed once pjax supports loading only missing css files + $view->cssFiles = null; + + $view->endPage(true); + + $content = ob_get_clean(); + + // only need the content enclosed within this widget + $response = Yii::$app->getResponse(); + $level = ob_get_level(); + $response->clearOutputBuffers(); + $response->setStatusCode(200); + $response->format = Response::FORMAT_HTML; + $response->content = $content; + $response->send(); + + // re-enable output buffer to capture content after this widget + for (; $level > 0; --$level) { + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * @return boolean whether the current request requires pjax response from this widget + */ + protected function requiresPjax() + { + $headers = Yii::$app->getRequest()->getHeaders(); + + return $headers->get('X-Pjax') && $headers->get('X-Pjax-Container') === '#' . $this->getId(); + } + + /** + * Registers the needed JavaScript. + */ + public function registerClientScript() + { + $id = $this->options['id']; + $this->clientOptions['push'] = $this->enablePushState; + $this->clientOptions['replace'] = $this->enableReplaceState; + $this->clientOptions['timeout'] = $this->timeout; + $this->clientOptions['scrollTo'] = $this->scrollTo; + $options = Json::encode($this->clientOptions); + $linkSelector = Json::encode($this->linkSelector !== null ? $this->linkSelector : '#' . $id . ' a'); + $formSelector = Json::encode($this->formSelector !== null ? $this->formSelector : '#' . $id . ' form[data-pjax]'); + $view = $this->getView(); + PjaxAsset::register($view); + $js = "jQuery(document).pjax($linkSelector, \"#$id\", $options);"; + $js .= "\njQuery(document).on('submit', $formSelector, function (event) {jQuery.pjax.submit(event, '#$id', $options);});"; + $view->registerJs($js); + } } diff --git a/framework/widgets/PjaxAsset.php b/framework/widgets/PjaxAsset.php index 3fcaf67af8b..4844f797fb5 100644 --- a/framework/widgets/PjaxAsset.php +++ b/framework/widgets/PjaxAsset.php @@ -17,11 +17,11 @@ */ class PjaxAsset extends AssetBundle { - public $sourcePath = '@vendor/yiisoft/jquery-pjax'; - public $js = [ - 'jquery.pjax.js', - ]; - public $depends = [ - 'yii\web\YiiAsset', - ]; + public $sourcePath = '@vendor/yiisoft/jquery-pjax'; + public $js = [ + 'jquery.pjax.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; } diff --git a/framework/widgets/Spaceless.php b/framework/widgets/Spaceless.php index 8115f856ca6..50f402e18a1 100644 --- a/framework/widgets/Spaceless.php +++ b/framework/widgets/Spaceless.php @@ -49,21 +49,21 @@ */ class Spaceless extends Widget { - /** - * Starts capturing an output to be cleaned from whitespace characters between HTML tags. - */ - public function init() - { - ob_start(); - ob_implicit_flush(false); - } + /** + * Starts capturing an output to be cleaned from whitespace characters between HTML tags. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } - /** - * Marks the end of content to be cleaned from whitespace characters between HTML tags. - * Stops capturing an output and echoes cleaned result. - */ - public function run() - { - echo trim(preg_replace('/>\s+<', ob_get_clean())); - } + /** + * Marks the end of content to be cleaned from whitespace characters between HTML tags. + * Stops capturing an output and echoes cleaned result. + */ + public function run() + { + echo trim(preg_replace('/>\s+<', ob_get_clean())); + } } diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php index eaa3b2886e1..37b4313f09b 100644 --- a/tests/unit/TestCase.php +++ b/tests/unit/TestCase.php @@ -9,54 +9,55 @@ */ abstract class TestCase extends \PHPUnit_Framework_TestCase { - public static $params; - - /** - * Clean up after test. - * By default the application created with [[mockApplication]] will be destroyed. - */ - protected function tearDown() - { - parent::tearDown(); - $this->destroyApplication(); - } - - /** - * Returns a test configuration param from /data/config.php - * @param string $name params name - * @param mixed $default default value to use when param is not set. - * @return mixed the value of the configuration param - */ - public function getParam($name, $default = null) - { - if (static::$params === null) { - static::$params = require(__DIR__ . '/data/config.php'); - } - return isset(static::$params[$name]) ? static::$params[$name] : $default; - } - - /** - * Populates Yii::$app with a new application - * The application will be destroyed on tearDown() automatically. - * @param array $config The application configuration, if needed - * @param string $appClass name of the application class to create - */ - protected function mockApplication($config = [], $appClass = '\yii\console\Application') - { - static $defaultConfig = [ - 'id' => 'testapp', - 'basePath' => __DIR__, - ]; - $defaultConfig['vendorPath'] = dirname(dirname(__DIR__)) . '/vendor'; - - new $appClass(ArrayHelper::merge($defaultConfig, $config)); - } - - /** - * Destroys application in Yii::$app by setting it to null. - */ - protected function destroyApplication() - { - \Yii::$app = null; - } + public static $params; + + /** + * Clean up after test. + * By default the application created with [[mockApplication]] will be destroyed. + */ + protected function tearDown() + { + parent::tearDown(); + $this->destroyApplication(); + } + + /** + * Returns a test configuration param from /data/config.php + * @param string $name params name + * @param mixed $default default value to use when param is not set. + * @return mixed the value of the configuration param + */ + public function getParam($name, $default = null) + { + if (static::$params === null) { + static::$params = require(__DIR__ . '/data/config.php'); + } + + return isset(static::$params[$name]) ? static::$params[$name] : $default; + } + + /** + * Populates Yii::$app with a new application + * The application will be destroyed on tearDown() automatically. + * @param array $config The application configuration, if needed + * @param string $appClass name of the application class to create + */ + protected function mockApplication($config = [], $appClass = '\yii\console\Application') + { + static $defaultConfig = [ + 'id' => 'testapp', + 'basePath' => __DIR__, + ]; + $defaultConfig['vendorPath'] = dirname(dirname(__DIR__)) . '/vendor'; + + new $appClass(ArrayHelper::merge($defaultConfig, $config)); + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + \Yii::$app = null; + } } diff --git a/tests/unit/VendorTestCase.php b/tests/unit/VendorTestCase.php index cffc1e0833b..c8f76716a0c 100644 --- a/tests/unit/VendorTestCase.php +++ b/tests/unit/VendorTestCase.php @@ -11,24 +11,24 @@ */ class VendorTestCase extends TestCase { - /** - * This method is called before the first test of this test class is run. - * Attempts to load vendor autoloader. - * @throws \yii\base\NotSupportedException - */ - public static function setUpBeforeClass() - { - $vendorDir = __DIR__ . '/../../vendor'; - if (!is_dir($vendorDir)) { - // this is used by `yii2-dev` - $vendorDir = __DIR__ . '/../../../../../vendor'; - } - Yii::setAlias('@vendor', $vendorDir); - $vendorAutoload = $vendorDir . '/autoload.php'; - if (file_exists($vendorAutoload)) { - require_once($vendorAutoload); - } else { - throw new NotSupportedException("Vendor autoload file '{$vendorAutoload}' is missing."); - } - } + /** + * This method is called before the first test of this test class is run. + * Attempts to load vendor autoloader. + * @throws \yii\base\NotSupportedException + */ + public static function setUpBeforeClass() + { + $vendorDir = __DIR__ . '/../../vendor'; + if (!is_dir($vendorDir)) { + // this is used by `yii2-dev` + $vendorDir = __DIR__ . '/../../../../../vendor'; + } + Yii::setAlias('@vendor', $vendorDir); + $vendorAutoload = $vendorDir . '/autoload.php'; + if (file_exists($vendorAutoload)) { + require_once($vendorAutoload); + } else { + throw new NotSupportedException("Vendor autoload file '{$vendorAutoload}' is missing."); + } + } } diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 66290ffa408..6e84928ec29 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -11,7 +11,7 @@ // require composer autoloader if available $composerAutoload = __DIR__ . '/../../vendor/autoload.php'; if (is_file($composerAutoload)) { - require_once($composerAutoload); + require_once($composerAutoload); } require_once(__DIR__ . '/../../framework/Yii.php'); diff --git a/tests/unit/data/ar/ActiveRecord.php b/tests/unit/data/ar/ActiveRecord.php index bcb5b48377d..0862c50c651 100644 --- a/tests/unit/data/ar/ActiveRecord.php +++ b/tests/unit/data/ar/ActiveRecord.php @@ -15,10 +15,10 @@ */ class ActiveRecord extends \yii\db\ActiveRecord { - public static $db; + public static $db; - public static function getDb() - { - return self::$db; - } + public static function getDb() + { + return self::$db; + } } diff --git a/tests/unit/data/ar/Category.php b/tests/unit/data/ar/Category.php index cebacb0c1bc..94b3ba18ece 100644 --- a/tests/unit/data/ar/Category.php +++ b/tests/unit/data/ar/Category.php @@ -15,13 +15,13 @@ */ class Category extends ActiveRecord { - public static function tableName() - { - return 'tbl_category'; - } + public static function tableName() + { + return 'tbl_category'; + } - public function getItems() - { - return $this->hasMany(Item::className(), ['category_id' => 'id']); - } + public function getItems() + { + return $this->hasMany(Item::className(), ['category_id' => 'id']); + } } diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 59a7e8865ae..c99190bf099 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -18,52 +18,54 @@ */ class Customer extends ActiveRecord { - const STATUS_ACTIVE = 1; - const STATUS_INACTIVE = 2; + const STATUS_ACTIVE = 1; + const STATUS_INACTIVE = 2; - public $status2; + public $status2; - public static function tableName() - { - return 'tbl_customer'; - } + public static function tableName() + { + return 'tbl_customer'; + } - public function getProfile() - { - return $this->hasOne(Profile::className(), ['id' => 'profile_id']); - } + public function getProfile() + { + return $this->hasOne(Profile::className(), ['id' => 'profile_id']); + } - public function getOrders() - { - return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id'); - } + public function getOrders() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id'); + } - public function getOrders2() - { - return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer2')->orderBy('id'); - } + public function getOrders2() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer2')->orderBy('id'); + } - // deeply nested table relation - public function getOrderItems() - { - /** @var ActiveQuery $rel */ - $rel = $this->hasMany(Item::className(), ['id' => 'item_id']); - return $rel->viaTable('tbl_order_item', ['order_id' => 'id'], function ($q) { - /** @var ActiveQuery $q */ - $q->viaTable('tbl_order', ['customer_id' => 'id']); - })->orderBy('id'); - } + // deeply nested table relation + public function getOrderItems() + { + /** @var ActiveQuery $rel */ + $rel = $this->hasMany(Item::className(), ['id' => 'item_id']); - public function afterSave($insert) - { - ActiveRecordTest::$afterSaveInsert = $insert; - ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; - parent::afterSave($insert); - } + return $rel->viaTable('tbl_order_item', ['order_id' => 'id'], function ($q) { + /** @var ActiveQuery $q */ + $q->viaTable('tbl_order', ['customer_id' => 'id']); + })->orderBy('id'); + } - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new CustomerQuery($config); - } + public function afterSave($insert) + { + ActiveRecordTest::$afterSaveInsert = $insert; + ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; + parent::afterSave($insert); + } + + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new CustomerQuery($config); + } } diff --git a/tests/unit/data/ar/CustomerQuery.php b/tests/unit/data/ar/CustomerQuery.php index c7086e08779..4a7b7736b28 100644 --- a/tests/unit/data/ar/CustomerQuery.php +++ b/tests/unit/data/ar/CustomerQuery.php @@ -9,9 +9,10 @@ */ class CustomerQuery extends ActiveQuery { - public function active() - { - $this->andWhere('status=1'); - return $this; - } + public function active() + { + $this->andWhere('status=1'); + + return $this; + } } diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index 2d04f9e8054..3161cbb2fa3 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -11,13 +11,13 @@ */ class Item extends ActiveRecord { - public static function tableName() - { - return 'tbl_item'; - } + public static function tableName() + { + return 'tbl_item'; + } - public function getCategory() - { - return $this->hasOne(Category::className(), ['id' => 'category_id']); - } + public function getCategory() + { + return $this->hasOne(Category::className(), ['id' => 'category_id']); + } } diff --git a/tests/unit/data/ar/NullValues.php b/tests/unit/data/ar/NullValues.php index e6aa3b988ba..07a9014ecc5 100644 --- a/tests/unit/data/ar/NullValues.php +++ b/tests/unit/data/ar/NullValues.php @@ -13,8 +13,8 @@ */ class NullValues extends ActiveRecord { - public static function tableName() - { - return 'tbl_null_values'; - } + public static function tableName() + { + return 'tbl_null_values'; + } } diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index d46637e1bbb..601c8c48a00 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -12,71 +12,72 @@ */ class Order extends ActiveRecord { - public static function tableName() - { - return 'tbl_order'; - } + public static function tableName() + { + return 'tbl_order'; + } - public function getCustomer() - { - return $this->hasOne(Customer::className(), ['id' => 'customer_id']); - } + public function getCustomer() + { + return $this->hasOne(Customer::className(), ['id' => 'customer_id']); + } - public function getCustomer2() - { - return $this->hasOne(Customer::className(), ['id' => 'customer_id'])->inverseOf('orders2'); - } + public function getCustomer2() + { + return $this->hasOne(Customer::className(), ['id' => 'customer_id'])->inverseOf('orders2'); + } - public function getOrderItems() - { - return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); - } + public function getOrderItems() + { + return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); + } - public function getItems() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - // additional query configuration - })->orderBy('id'); - } + public function getItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + // additional query configuration + })->orderBy('id'); + } - public function getItemsInOrder1() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_ASC]); - })->orderBy('name'); - } + public function getItemsInOrder1() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_ASC]); + })->orderBy('name'); + } - public function getItemsInOrder2() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_DESC]); - })->orderBy('name'); - } + public function getItemsInOrder2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_DESC]); + })->orderBy('name'); + } - public function getBooks() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->viaTable('tbl_order_item', ['order_id' => 'id']) - ->where(['category_id' => 1]); - } + public function getBooks() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->viaTable('tbl_order_item', ['order_id' => 'id']) + ->where(['category_id' => 1]); + } - public function getBooks2() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->onCondition(['category_id' => 1]) - ->viaTable('tbl_order_item', ['order_id' => 'id']); - } + public function getBooks2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->onCondition(['category_id' => 1]) + ->viaTable('tbl_order_item', ['order_id' => 'id']); + } - public function beforeSave($insert) - { - if (parent::beforeSave($insert)) { - $this->created_at = time(); - return true; - } else { - return false; - } - } + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + $this->created_at = time(); + + return true; + } else { + return false; + } + } } diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index b340a460db4..e562460ac41 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -12,18 +12,18 @@ */ class OrderItem extends ActiveRecord { - public static function tableName() - { - return 'tbl_order_item'; - } + public static function tableName() + { + return 'tbl_order_item'; + } - public function getOrder() - { - return $this->hasOne(Order::className(), ['id' => 'order_id']); - } + public function getOrder() + { + return $this->hasOne(Order::className(), ['id' => 'order_id']); + } - public function getItem() - { - return $this->hasOne(Item::className(), ['id' => 'item_id']); - } + public function getItem() + { + return $this->hasOne(Item::className(), ['id' => 'item_id']); + } } diff --git a/tests/unit/data/ar/Profile.php b/tests/unit/data/ar/Profile.php index 3274f2dfa9f..272f11ab1da 100644 --- a/tests/unit/data/ar/Profile.php +++ b/tests/unit/data/ar/Profile.php @@ -14,8 +14,8 @@ */ class Profile extends ActiveRecord { - public static function tableName() - { - return 'tbl_profile'; - } + public static function tableName() + { + return 'tbl_profile'; + } } diff --git a/tests/unit/data/ar/elasticsearch/ActiveRecord.php b/tests/unit/data/ar/elasticsearch/ActiveRecord.php index aa1f304a1be..1c110368d3a 100644 --- a/tests/unit/data/ar/elasticsearch/ActiveRecord.php +++ b/tests/unit/data/ar/elasticsearch/ActiveRecord.php @@ -15,18 +15,18 @@ */ class ActiveRecord extends \yii\elasticsearch\ActiveRecord { - public static $db; + public static $db; - /** - * @return \yii\elasticsearch\Connection - */ - public static function getDb() - { - return self::$db; - } + /** + * @return \yii\elasticsearch\Connection + */ + public static function getDb() + { + return self::$db; + } - public static function index() - { - return 'yiitest'; - } + public static function index() + { + return 'yiitest'; + } } diff --git a/tests/unit/data/ar/elasticsearch/Customer.php b/tests/unit/data/ar/elasticsearch/Customer.php index 99165c74884..6f058a5fc28 100644 --- a/tests/unit/data/ar/elasticsearch/Customer.php +++ b/tests/unit/data/ar/elasticsearch/Customer.php @@ -15,58 +15,59 @@ */ class Customer extends ActiveRecord { - const STATUS_ACTIVE = 1; - const STATUS_INACTIVE = 2; + const STATUS_ACTIVE = 1; + const STATUS_INACTIVE = 2; - public $status2; + public $status2; - public static function primaryKey() - { - return ['id']; - } + public static function primaryKey() + { + return ['id']; + } - public function attributes() - { - return ['id', 'name', 'email', 'address', 'status']; - } + public function attributes() + { + return ['id', 'name', 'email', 'address', 'status']; + } - public function getOrders() - { - return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('created_at'); - } + public function getOrders() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('created_at'); + } - public function afterSave($insert) - { - ActiveRecordTest::$afterSaveInsert = $insert; - ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; - parent::afterSave($insert); - } + public function afterSave($insert) + { + ActiveRecordTest::$afterSaveInsert = $insert; + ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; + parent::afterSave($insert); + } - /** - * sets up the index for this record - * @param Command $command - * @param boolean $statusIsBoolean - */ - public static function setUpMapping($command, $statusIsBoolean = false) - { - $command->deleteMapping(static::index(), static::type()); - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - "_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"], - "properties" => [ - "name" => ["type" => "string", "index" => "not_analyzed"], - "email" => ["type" => "string", "index" => "not_analyzed"], - "address" => ["type" => "string", "index" => "analyzed"], - "status" => $statusIsBoolean ? ["type" => "boolean"] : ["type" => "integer"], - ] - ] - ]); + /** + * sets up the index for this record + * @param Command $command + * @param boolean $statusIsBoolean + */ + public static function setUpMapping($command, $statusIsBoolean = false) + { + $command->deleteMapping(static::index(), static::type()); + $command->setMapping(static::index(), static::type(), [ + static::type() => [ + "_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"], + "properties" => [ + "name" => ["type" => "string", "index" => "not_analyzed"], + "email" => ["type" => "string", "index" => "not_analyzed"], + "address" => ["type" => "string", "index" => "analyzed"], + "status" => $statusIsBoolean ? ["type" => "boolean"] : ["type" => "integer"], + ] + ] + ]); - } + } - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new CustomerQuery($config); - } + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new CustomerQuery($config); + } } diff --git a/tests/unit/data/ar/elasticsearch/CustomerQuery.php b/tests/unit/data/ar/elasticsearch/CustomerQuery.php index 0da53054ef0..746a2ec1a42 100644 --- a/tests/unit/data/ar/elasticsearch/CustomerQuery.php +++ b/tests/unit/data/ar/elasticsearch/CustomerQuery.php @@ -9,9 +9,10 @@ */ class CustomerQuery extends ActiveQuery { - public function active() - { - $this->andWhere(['status' => 1]); - return $this; - } + public function active() + { + $this->andWhere(['status' => 1]); + + return $this; + } } diff --git a/tests/unit/data/ar/elasticsearch/Item.php b/tests/unit/data/ar/elasticsearch/Item.php index 2baf62fc6cc..283ac7b3619 100644 --- a/tests/unit/data/ar/elasticsearch/Item.php +++ b/tests/unit/data/ar/elasticsearch/Item.php @@ -13,32 +13,32 @@ */ class Item extends ActiveRecord { - public static function primaryKey() - { - return ['id']; - } + public static function primaryKey() + { + return ['id']; + } - public function attributes() - { - return ['id', 'name', 'category_id']; - } + public function attributes() + { + return ['id', 'name', 'category_id']; + } - /** - * sets up the index for this record - * @param Command $command - */ - public static function setUpMapping($command) - { - $command->deleteMapping(static::index(), static::type()); - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - "_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"], - "properties" => [ - "name" => ["type" => "string", "index" => "not_analyzed"], - "category_id" => ["type" => "integer"], - ] - ] - ]); + /** + * sets up the index for this record + * @param Command $command + */ + public static function setUpMapping($command) + { + $command->deleteMapping(static::index(), static::type()); + $command->setMapping(static::index(), static::type(), [ + static::type() => [ + "_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"], + "properties" => [ + "name" => ["type" => "string", "index" => "not_analyzed"], + "category_id" => ["type" => "integer"], + ] + ] + ]); - } + } } diff --git a/tests/unit/data/ar/elasticsearch/Order.php b/tests/unit/data/ar/elasticsearch/Order.php index 1bf76bbaa2d..bf52f633000 100644 --- a/tests/unit/data/ar/elasticsearch/Order.php +++ b/tests/unit/data/ar/elasticsearch/Order.php @@ -14,47 +14,47 @@ */ class Order extends ActiveRecord { - public static function primaryKey() - { - return ['id']; - } + public static function primaryKey() + { + return ['id']; + } - public function attributes() - { - return ['id', 'customer_id', 'created_at', 'total']; - } + public function attributes() + { + return ['id', 'customer_id', 'created_at', 'total']; + } - public function getCustomer() - { - return $this->hasOne(Customer::className(), ['id' => 'customer_id']); - } + public function getCustomer() + { + return $this->hasOne(Customer::className(), ['id' => 'customer_id']); + } - public function getOrderItems() - { - return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); - } + public function getOrderItems() + { + return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); + } - public function getItems() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems')->orderBy('id'); - } + public function getItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems')->orderBy('id'); + } - public function getItemsInOrder1() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_ASC]); - })->orderBy('name'); - } + public function getItemsInOrder1() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_ASC]); + })->orderBy('name'); + } - public function getItemsInOrder2() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_DESC]); - })->orderBy('name'); - } + public function getItemsInOrder2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_DESC]); + })->orderBy('name'); + } // public function getBooks() // { @@ -63,33 +63,33 @@ public function getItemsInOrder2() // ->where(['category_id' => 1]); // } - public function beforeSave($insert) - { - if (parent::beforeSave($insert)) { + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { // $this->created_at = time(); - return true; - } else { - return false; - } - } + return true; + } else { + return false; + } + } - /** - * sets up the index for this record - * @param Command $command - */ - public static function setUpMapping($command) - { - $command->deleteMapping(static::index(), static::type()); - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - "_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"], - "properties" => [ - "customer_id" => ["type" => "integer"], + /** + * sets up the index for this record + * @param Command $command + */ + public static function setUpMapping($command) + { + $command->deleteMapping(static::index(), static::type()); + $command->setMapping(static::index(), static::type(), [ + static::type() => [ + "_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"], + "properties" => [ + "customer_id" => ["type" => "integer"], // "created_at" => ["type" => "string", "index" => "not_analyzed"], - "total" => ["type" => "integer"], - ] - ] - ]); + "total" => ["type" => "integer"], + ] + ] + ]); - } + } } diff --git a/tests/unit/data/ar/elasticsearch/OrderItem.php b/tests/unit/data/ar/elasticsearch/OrderItem.php index b8cdf400d9b..1e1b6e68597 100644 --- a/tests/unit/data/ar/elasticsearch/OrderItem.php +++ b/tests/unit/data/ar/elasticsearch/OrderItem.php @@ -14,38 +14,38 @@ */ class OrderItem extends ActiveRecord { - public function attributes() - { - return ['order_id', 'item_id', 'quantity', 'subtotal']; - } + public function attributes() + { + return ['order_id', 'item_id', 'quantity', 'subtotal']; + } - public function getOrder() - { - return $this->hasOne(Order::className(), ['id' => 'order_id']); - } + public function getOrder() + { + return $this->hasOne(Order::className(), ['id' => 'order_id']); + } - public function getItem() - { - return $this->hasOne(Item::className(), ['id' => 'item_id']); - } + public function getItem() + { + return $this->hasOne(Item::className(), ['id' => 'item_id']); + } - /** - * sets up the index for this record - * @param Command $command - */ - public static function setUpMapping($command) - { - $command->deleteMapping(static::index(), static::type()); - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - "properties" => [ - "order_id" => ["type" => "integer"], - "item_id" => ["type" => "integer"], - "quantity" => ["type" => "integer"], - "subtotal" => ["type" => "integer"], - ] - ] - ]); + /** + * sets up the index for this record + * @param Command $command + */ + public static function setUpMapping($command) + { + $command->deleteMapping(static::index(), static::type()); + $command->setMapping(static::index(), static::type(), [ + static::type() => [ + "properties" => [ + "order_id" => ["type" => "integer"], + "item_id" => ["type" => "integer"], + "quantity" => ["type" => "integer"], + "subtotal" => ["type" => "integer"], + ] + ] + ]); - } + } } diff --git a/tests/unit/data/ar/mongodb/ActiveRecord.php b/tests/unit/data/ar/mongodb/ActiveRecord.php index 47b4cd9c608..8fb44913ed3 100644 --- a/tests/unit/data/ar/mongodb/ActiveRecord.php +++ b/tests/unit/data/ar/mongodb/ActiveRecord.php @@ -7,10 +7,10 @@ */ class ActiveRecord extends \yii\mongodb\ActiveRecord { - public static $db; + public static $db; - public static function getDb() - { - return self::$db; - } + public static function getDb() + { + return self::$db; + } } diff --git a/tests/unit/data/ar/mongodb/Customer.php b/tests/unit/data/ar/mongodb/Customer.php index 8fa2a2311c3..268d93115ee 100644 --- a/tests/unit/data/ar/mongodb/Customer.php +++ b/tests/unit/data/ar/mongodb/Customer.php @@ -4,30 +4,31 @@ class Customer extends ActiveRecord { - public static function collectionName() - { - return 'customer'; - } + public static function collectionName() + { + return 'customer'; + } - public function attributes() - { - return [ - '_id', - 'name', - 'email', - 'address', - 'status', - ]; - } + public function attributes() + { + return [ + '_id', + 'name', + 'email', + 'address', + 'status', + ]; + } - public function getOrders() - { - return $this->hasMany(CustomerOrder::className(), ['customer_id' => '_id']); - } + public function getOrders() + { + return $this->hasMany(CustomerOrder::className(), ['customer_id' => '_id']); + } - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new CustomerQuery($config); - } + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new CustomerQuery($config); + } } diff --git a/tests/unit/data/ar/mongodb/CustomerOrder.php b/tests/unit/data/ar/mongodb/CustomerOrder.php index ecac6666ed8..8016bb018cc 100644 --- a/tests/unit/data/ar/mongodb/CustomerOrder.php +++ b/tests/unit/data/ar/mongodb/CustomerOrder.php @@ -2,26 +2,25 @@ namespace yiiunit\data\ar\mongodb; - class CustomerOrder extends ActiveRecord { - public static function collectionName() - { - return 'customer_order'; - } + public static function collectionName() + { + return 'customer_order'; + } - public function attributes() - { - return [ - '_id', - 'number', - 'customer_id', - 'items', - ]; - } + public function attributes() + { + return [ + '_id', + 'number', + 'customer_id', + 'items', + ]; + } - public function getCustomer() - { - return $this->hasOne(Customer::className(), ['_id' => 'customer_id']); - } + public function getCustomer() + { + return $this->hasOne(Customer::className(), ['_id' => 'customer_id']); + } } diff --git a/tests/unit/data/ar/mongodb/CustomerQuery.php b/tests/unit/data/ar/mongodb/CustomerQuery.php index 3b2d93d538f..e7a9887614d 100644 --- a/tests/unit/data/ar/mongodb/CustomerQuery.php +++ b/tests/unit/data/ar/mongodb/CustomerQuery.php @@ -9,9 +9,10 @@ */ class CustomerQuery extends ActiveQuery { - public function activeOnly() - { - $this->andWhere(['status' => 2]); - return $this; - } + public function activeOnly() + { + $this->andWhere(['status' => 2]); + + return $this; + } } diff --git a/tests/unit/data/ar/mongodb/file/ActiveRecord.php b/tests/unit/data/ar/mongodb/file/ActiveRecord.php index 270ba43b7b1..08a1921c2fc 100644 --- a/tests/unit/data/ar/mongodb/file/ActiveRecord.php +++ b/tests/unit/data/ar/mongodb/file/ActiveRecord.php @@ -7,10 +7,10 @@ */ class ActiveRecord extends \yii\mongodb\file\ActiveRecord { - public static $db; + public static $db; - public static function getDb() - { - return self::$db; - } + public static function getDb() + { + return self::$db; + } } diff --git a/tests/unit/data/ar/mongodb/file/CustomerFile.php b/tests/unit/data/ar/mongodb/file/CustomerFile.php index 3083ff6373e..f3be6585e5e 100644 --- a/tests/unit/data/ar/mongodb/file/CustomerFile.php +++ b/tests/unit/data/ar/mongodb/file/CustomerFile.php @@ -4,25 +4,26 @@ class CustomerFile extends ActiveRecord { - public static function collectionName() - { - return 'customer_fs'; - } + public static function collectionName() + { + return 'customer_fs'; + } - public function attributes() - { - return array_merge( - parent::attributes(), - [ - 'tag', - 'status', - ] - ); - } + public function attributes() + { + return array_merge( + parent::attributes(), + [ + 'tag', + 'status', + ] + ); + } - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new CustomerFileQuery($config); - } + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new CustomerFileQuery($config); + } } diff --git a/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php b/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php index f49f644659e..44346f1cbb2 100644 --- a/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php +++ b/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php @@ -9,9 +9,10 @@ */ class CustomerFileQuery extends ActiveQuery { - public function activeOnly() - { - $this->andWhere(['status' => 2]); - return $this; - } + public function activeOnly() + { + $this->andWhere(['status' => 2]); + + return $this; + } } diff --git a/tests/unit/data/ar/redis/ActiveRecord.php b/tests/unit/data/ar/redis/ActiveRecord.php index 121cd897503..0e1cc935f32 100644 --- a/tests/unit/data/ar/redis/ActiveRecord.php +++ b/tests/unit/data/ar/redis/ActiveRecord.php @@ -15,10 +15,10 @@ */ class ActiveRecord extends \yii\redis\ActiveRecord { - public static $db; + public static $db; - public static function getDb() - { - return self::$db; - } + public static function getDb() + { + return self::$db; + } } diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php index 06ad93af31a..637cea6ff29 100644 --- a/tests/unit/data/ar/redis/Customer.php +++ b/tests/unit/data/ar/redis/Customer.php @@ -6,34 +6,35 @@ class Customer extends ActiveRecord { - const STATUS_ACTIVE = 1; - const STATUS_INACTIVE = 2; - - public $status2; - - public function attributes() - { - return ['id', 'email', 'name', 'address', 'status', 'profile_id']; - } - - /** - * @return \yii\redis\ActiveQuery - */ - public function getOrders() - { - return $this->hasMany(Order::className(), ['customer_id' => 'id']); - } - - public function afterSave($insert) - { - ActiveRecordTest::$afterSaveInsert = $insert; - ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; - parent::afterSave($insert); - } - - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new CustomerQuery($config); - } + const STATUS_ACTIVE = 1; + const STATUS_INACTIVE = 2; + + public $status2; + + public function attributes() + { + return ['id', 'email', 'name', 'address', 'status', 'profile_id']; + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrders() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id']); + } + + public function afterSave($insert) + { + ActiveRecordTest::$afterSaveInsert = $insert; + ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; + parent::afterSave($insert); + } + + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new CustomerQuery($config); + } } diff --git a/tests/unit/data/ar/redis/CustomerQuery.php b/tests/unit/data/ar/redis/CustomerQuery.php index 4e68d32dc4d..fc564a05367 100644 --- a/tests/unit/data/ar/redis/CustomerQuery.php +++ b/tests/unit/data/ar/redis/CustomerQuery.php @@ -9,9 +9,10 @@ */ class CustomerQuery extends ActiveQuery { - public function active() - { - $this->andWhere(['status' => 1]); - return $this; - } + public function active() + { + $this->andWhere(['status' => 1]); + + return $this; + } } diff --git a/tests/unit/data/ar/redis/Item.php b/tests/unit/data/ar/redis/Item.php index 81daa638e35..b06ba800b11 100644 --- a/tests/unit/data/ar/redis/Item.php +++ b/tests/unit/data/ar/redis/Item.php @@ -4,8 +4,8 @@ class Item extends ActiveRecord { - public function attributes() - { - return ['id', 'name', 'category_id']; - } + public function attributes() + { + return ['id', 'name', 'category_id']; + } } diff --git a/tests/unit/data/ar/redis/Order.php b/tests/unit/data/ar/redis/Order.php index 2a491ae6dcc..98de6f1fb52 100644 --- a/tests/unit/data/ar/redis/Order.php +++ b/tests/unit/data/ar/redis/Order.php @@ -4,59 +4,60 @@ class Order extends ActiveRecord { - public function attributes() - { - return ['id', 'customer_id', 'created_at', 'total']; - } - - public function getCustomer() - { - return $this->hasOne(Customer::className(), ['id' => 'customer_id']); - } - - public function getOrderItems() - { - return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); - } - - public function getItems() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - // additional query configuration - }); - } - - public function getItemsInOrder1() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_ASC]); - })->orderBy('name'); - } - - public function getItemsInOrder2() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_DESC]); - })->orderBy('name'); - } - - public function getBooks() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', ['order_id' => 'id']); - //->where(['category_id' => 1]); - } - - public function beforeSave($insert) - { - if (parent::beforeSave($insert)) { - $this->created_at = time(); - return true; - } else { - return false; - } - } + public function attributes() + { + return ['id', 'customer_id', 'created_at', 'total']; + } + + public function getCustomer() + { + return $this->hasOne(Customer::className(), ['id' => 'customer_id']); + } + + public function getOrderItems() + { + return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); + } + + public function getItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + // additional query configuration + }); + } + + public function getItemsInOrder1() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_ASC]); + })->orderBy('name'); + } + + public function getItemsInOrder2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_DESC]); + })->orderBy('name'); + } + + public function getBooks() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', ['order_id' => 'id']); + //->where(['category_id' => 1]); + } + + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + $this->created_at = time(); + + return true; + } else { + return false; + } + } } diff --git a/tests/unit/data/ar/redis/OrderItem.php b/tests/unit/data/ar/redis/OrderItem.php index 36a290fb682..d65643d1673 100644 --- a/tests/unit/data/ar/redis/OrderItem.php +++ b/tests/unit/data/ar/redis/OrderItem.php @@ -4,23 +4,23 @@ class OrderItem extends ActiveRecord { - public static function primaryKey() - { - return ['order_id', 'item_id']; - } + public static function primaryKey() + { + return ['order_id', 'item_id']; + } - public function attributes() - { - return ['order_id', 'item_id', 'quantity', 'subtotal']; - } + public function attributes() + { + return ['order_id', 'item_id', 'quantity', 'subtotal']; + } - public function getOrder() - { - return $this->hasOne(Order::className(), ['id' => 'order_id']); - } + public function getOrder() + { + return $this->hasOne(Order::className(), ['id' => 'order_id']); + } - public function getItem() - { - return $this->hasOne(Item::className(), ['id' => 'item_id']); - } + public function getItem() + { + return $this->hasOne(Item::className(), ['id' => 'item_id']); + } } diff --git a/tests/unit/data/ar/sphinx/ActiveRecord.php b/tests/unit/data/ar/sphinx/ActiveRecord.php index 2df2821a3ef..12f95e36153 100644 --- a/tests/unit/data/ar/sphinx/ActiveRecord.php +++ b/tests/unit/data/ar/sphinx/ActiveRecord.php @@ -7,10 +7,10 @@ */ class ActiveRecord extends \yii\sphinx\ActiveRecord { - public static $db; + public static $db; - public static function getDb() - { - return self::$db; - } + public static function getDb() + { + return self::$db; + } } diff --git a/tests/unit/data/ar/sphinx/ArticleDb.php b/tests/unit/data/ar/sphinx/ArticleDb.php index 39dc3140736..c24b3e79c73 100644 --- a/tests/unit/data/ar/sphinx/ArticleDb.php +++ b/tests/unit/data/ar/sphinx/ArticleDb.php @@ -7,19 +7,20 @@ class ArticleDb extends ActiveRecordDb { - public static function tableName() - { - return 'yii2_test_article'; - } + public static function tableName() + { + return 'yii2_test_article'; + } - public function getIndex() - { - $config = [ - 'modelClass' => ArticleIndex::className(), - 'primaryModel' => $this, - 'link' => ['id' => 'id'], - 'multiple' => false, - ]; - return new ActiveQuery($config); - } + public function getIndex() + { + $config = [ + 'modelClass' => ArticleIndex::className(), + 'primaryModel' => $this, + 'link' => ['id' => 'id'], + 'multiple' => false, + ]; + + return new ActiveQuery($config); + } } diff --git a/tests/unit/data/ar/sphinx/ArticleIndex.php b/tests/unit/data/ar/sphinx/ArticleIndex.php index 5f710783f14..916ddafed13 100644 --- a/tests/unit/data/ar/sphinx/ArticleIndex.php +++ b/tests/unit/data/ar/sphinx/ArticleIndex.php @@ -3,31 +3,32 @@ class ArticleIndex extends ActiveRecord { - public $custom_column; + public $custom_column; - public static function indexName() - { - return 'yii2_test_article_index'; - } + public static function indexName() + { + return 'yii2_test_article_index'; + } - public function getSource() - { - return $this->hasOne(ArticleDb::className(), ['id' => 'id']); - } + public function getSource() + { + return $this->hasOne(ArticleDb::className(), ['id' => 'id']); + } - public function getTags() - { - return $this->hasMany(TagDb::className(), ['id' => 'tag']); - } + public function getTags() + { + return $this->hasMany(TagDb::className(), ['id' => 'tag']); + } - public function getSnippetSource() - { - return $this->source->content; - } + public function getSnippetSource() + { + return $this->source->content; + } - public static function createQuery($config = []) - { - $config['modelClass'] = get_called_class(); - return new ArticleIndexQuery($config); - } + public static function createQuery($config = []) + { + $config['modelClass'] = get_called_class(); + + return new ArticleIndexQuery($config); + } } diff --git a/tests/unit/data/ar/sphinx/ArticleIndexQuery.php b/tests/unit/data/ar/sphinx/ArticleIndexQuery.php index e3589d6e5d5..4f5b7a629ed 100644 --- a/tests/unit/data/ar/sphinx/ArticleIndexQuery.php +++ b/tests/unit/data/ar/sphinx/ArticleIndexQuery.php @@ -9,9 +9,10 @@ */ class ArticleIndexQuery extends ActiveQuery { - public function favoriteAuthor() - { - $this->andWhere('author_id=1'); - return $this; - } + public function favoriteAuthor() + { + $this->andWhere('author_id=1'); + + return $this; + } } diff --git a/tests/unit/data/ar/sphinx/ItemDb.php b/tests/unit/data/ar/sphinx/ItemDb.php index c2a40e36872..386813a1fd7 100644 --- a/tests/unit/data/ar/sphinx/ItemDb.php +++ b/tests/unit/data/ar/sphinx/ItemDb.php @@ -6,8 +6,8 @@ class ItemDb extends ActiveRecordDb { - public static function tableName() - { - return 'yii2_test_item'; - } + public static function tableName() + { + return 'yii2_test_item'; + } } diff --git a/tests/unit/data/ar/sphinx/ItemIndex.php b/tests/unit/data/ar/sphinx/ItemIndex.php index 64b46502b21..1a735084e2e 100644 --- a/tests/unit/data/ar/sphinx/ItemIndex.php +++ b/tests/unit/data/ar/sphinx/ItemIndex.php @@ -4,8 +4,8 @@ class ItemIndex extends ActiveRecord { - public static function indexName() - { - return 'yii2_test_item_index'; - } + public static function indexName() + { + return 'yii2_test_item_index'; + } } diff --git a/tests/unit/data/ar/sphinx/RuntimeIndex.php b/tests/unit/data/ar/sphinx/RuntimeIndex.php index 0a725e3d4e4..095ad1c8080 100644 --- a/tests/unit/data/ar/sphinx/RuntimeIndex.php +++ b/tests/unit/data/ar/sphinx/RuntimeIndex.php @@ -4,8 +4,8 @@ class RuntimeIndex extends ActiveRecord { - public static function indexName() - { - return 'yii2_test_rt_index'; - } + public static function indexName() + { + return 'yii2_test_rt_index'; + } } diff --git a/tests/unit/data/ar/sphinx/TagDb.php b/tests/unit/data/ar/sphinx/TagDb.php index 126e54b666f..6b28ced4eb2 100644 --- a/tests/unit/data/ar/sphinx/TagDb.php +++ b/tests/unit/data/ar/sphinx/TagDb.php @@ -5,8 +5,8 @@ class TagDb extends ActiveRecordDb { - public static function tableName() - { - return 'yii2_test_tag'; - } + public static function tableName() + { + return 'yii2_test_tag'; + } } diff --git a/tests/unit/data/base/InvalidRulesModel.php b/tests/unit/data/base/InvalidRulesModel.php index 3c865e5849e..dc6a9c65428 100644 --- a/tests/unit/data/base/InvalidRulesModel.php +++ b/tests/unit/data/base/InvalidRulesModel.php @@ -8,10 +8,10 @@ */ class InvalidRulesModel extends Model { - public function rules() - { - return [ - ['test'], - ]; - } + public function rules() + { + return [ + ['test'], + ]; + } } diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php index 547cee8e2ad..c6a999f68aa 100644 --- a/tests/unit/data/base/Singer.php +++ b/tests/unit/data/base/Singer.php @@ -8,15 +8,15 @@ */ class Singer extends Model { - public $fistName; - public $lastName; + public $fistName; + public $lastName; - public function rules() - { - return [ - [['lastName'], 'default', 'value' => 'Lennon'], - [['lastName'], 'required'], - [['underscore_style'], 'yii\captcha\CaptchaValidator'], - ]; - } + public function rules() + { + return [ + [['lastName'], 'default', 'value' => 'Lennon'], + [['lastName'], 'required'], + [['underscore_style'], 'yii\captcha\CaptchaValidator'], + ]; + } } diff --git a/tests/unit/data/base/Speaker.php b/tests/unit/data/base/Speaker.php index 7585df3a19e..5e801110e93 100644 --- a/tests/unit/data/base/Speaker.php +++ b/tests/unit/data/base/Speaker.php @@ -8,38 +8,38 @@ */ class Speaker extends Model { - public $firstName; - public $lastName; - - public $customLabel; - public $underscore_style; - - protected $protectedProperty; - private $_privateProperty; - - public static $formName = 'Speaker'; - - public function formName() - { - return static::$formName; - } - - public function attributeLabels() - { - return [ - 'customLabel' => 'This is the custom label', - ]; - } - - public function rules() - { - return []; - } - - public function scenarios() - { - return [ - 'test' => ['firstName', 'lastName', '!underscore_style'], - ]; - } + public $firstName; + public $lastName; + + public $customLabel; + public $underscore_style; + + protected $protectedProperty; + private $_privateProperty; + + public static $formName = 'Speaker'; + + public function formName() + { + return static::$formName; + } + + public function attributeLabels() + { + return [ + 'customLabel' => 'This is the custom label', + ]; + } + + public function rules() + { + return []; + } + + public function scenarios() + { + return [ + 'test' => ['firstName', 'lastName', '!underscore_style'], + ]; + } } diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index a3dfdd426e7..0bbd8eb14b0 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -1,60 +1,60 @@ [ - 'cubrid' => [ - 'dsn' => 'cubrid:dbname=demodb;host=localhost;port=33000', - 'username' => 'dba', - 'password' => '', - 'fixture' => __DIR__ . '/cubrid.sql', - ], - 'mysql' => [ - 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', - 'username' => 'travis', - 'password' => '', - 'fixture' => __DIR__ . '/mysql.sql', - ], - 'sqlite' => [ - 'dsn' => 'sqlite::memory:', - 'fixture' => __DIR__ . '/sqlite.sql', - ], - 'sqlsrv' => [ - 'dsn' => 'sqlsrv:Server=localhost;Database=test', - 'username' => '', - 'password' => '', - 'fixture' => __DIR__ . '/mssql.sql', - ], - 'pgsql' => [ - 'dsn' => 'pgsql:host=localhost;dbname=yiitest;port=5432;', - 'username' => 'postgres', - 'password' => 'postgres', - 'fixture' => __DIR__ . '/postgres.sql', - ], - 'elasticsearch' => [ - 'dsn' => 'elasticsearch://localhost:9200' - ], - 'redis' => [ - 'hostname' => 'localhost', - 'port' => 6379, - 'database' => 0, - 'password' => null, - ], - ], - 'sphinx' => [ - 'sphinx' => [ - 'dsn' => 'mysql:host=127.0.0.1;port=9306;', - 'username' => 'travis', - 'password' => '', - ], - 'db' => [ - 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', - 'username' => 'travis', - 'password' => '', - 'fixture' => __DIR__ . '/sphinx/source.sql', - ], - ], - 'mongodb' => [ - 'dsn' => 'mongodb://travis:test@localhost:27017', - 'defaultDatabaseName' => 'yii2test', - 'options' => [], - ] + 'databases' => [ + 'cubrid' => [ + 'dsn' => 'cubrid:dbname=demodb;host=localhost;port=33000', + 'username' => 'dba', + 'password' => '', + 'fixture' => __DIR__ . '/cubrid.sql', + ], + 'mysql' => [ + 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', + 'username' => 'travis', + 'password' => '', + 'fixture' => __DIR__ . '/mysql.sql', + ], + 'sqlite' => [ + 'dsn' => 'sqlite::memory:', + 'fixture' => __DIR__ . '/sqlite.sql', + ], + 'sqlsrv' => [ + 'dsn' => 'sqlsrv:Server=localhost;Database=test', + 'username' => '', + 'password' => '', + 'fixture' => __DIR__ . '/mssql.sql', + ], + 'pgsql' => [ + 'dsn' => 'pgsql:host=localhost;dbname=yiitest;port=5432;', + 'username' => 'postgres', + 'password' => 'postgres', + 'fixture' => __DIR__ . '/postgres.sql', + ], + 'elasticsearch' => [ + 'dsn' => 'elasticsearch://localhost:9200' + ], + 'redis' => [ + 'hostname' => 'localhost', + 'port' => 6379, + 'database' => 0, + 'password' => null, + ], + ], + 'sphinx' => [ + 'sphinx' => [ + 'dsn' => 'mysql:host=127.0.0.1;port=9306;', + 'username' => 'travis', + 'password' => '', + ], + 'db' => [ + 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', + 'username' => 'travis', + 'password' => '', + 'fixture' => __DIR__ . '/sphinx/source.sql', + ], + ], + 'mongodb' => [ + 'dsn' => 'mongodb://travis:test@localhost:27017', + 'defaultDatabaseName' => 'yii2test', + 'options' => [], + ] ]; diff --git a/tests/unit/data/i18n/messages/de-DE/test.php b/tests/unit/data/i18n/messages/de-DE/test.php index 7cb3edef930..b0ab4193775 100644 --- a/tests/unit/data/i18n/messages/de-DE/test.php +++ b/tests/unit/data/i18n/messages/de-DE/test.php @@ -3,7 +3,7 @@ * */ return [ - 'The dog runs fast.' => 'Der Hund rennt schnell.', - 'His speed is about {n} km/h.' => 'Seine Geschwindigkeit beträgt {n} km/h.', - 'His name is {name} and his speed is about {n, number} km/h.' => 'Er heißt {name} und ist {n, number} km/h schnell.', + 'The dog runs fast.' => 'Der Hund rennt schnell.', + 'His speed is about {n} km/h.' => 'Seine Geschwindigkeit beträgt {n} km/h.', + 'His name is {name} and his speed is about {n, number} km/h.' => 'Er heißt {name} und ist {n, number} km/h schnell.', ]; diff --git a/tests/unit/data/i18n/messages/de/test.php b/tests/unit/data/i18n/messages/de/test.php index 9a8684c9c9f..6d7b903f554 100644 --- a/tests/unit/data/i18n/messages/de/test.php +++ b/tests/unit/data/i18n/messages/de/test.php @@ -3,5 +3,5 @@ * */ return [ - 'Hello world!' => 'Hallo Welt!', + 'Hello world!' => 'Hallo Welt!', ]; diff --git a/tests/unit/data/i18n/messages/en-US/test.php b/tests/unit/data/i18n/messages/en-US/test.php index 1f61a9be58d..d14d9ca5659 100644 --- a/tests/unit/data/i18n/messages/en-US/test.php +++ b/tests/unit/data/i18n/messages/en-US/test.php @@ -3,5 +3,5 @@ * */ return [ - 'The dog runs fast.' => 'Der Hund rennt schell.', + 'The dog runs fast.' => 'Der Hund rennt schell.', ]; diff --git a/tests/unit/data/i18n/messages/ru/test.php b/tests/unit/data/i18n/messages/ru/test.php index 6ce40dd5e00..8c5ed862070 100644 --- a/tests/unit/data/i18n/messages/ru/test.php +++ b/tests/unit/data/i18n/messages/ru/test.php @@ -3,5 +3,5 @@ * */ return [ - 'The dog runs fast.' => 'Собака бегает быстро.', + 'The dog runs fast.' => 'Собака бегает быстро.', ]; diff --git a/tests/unit/data/validators/TestValidator.php b/tests/unit/data/validators/TestValidator.php index 3bc6520efa7..b26a589fe0d 100644 --- a/tests/unit/data/validators/TestValidator.php +++ b/tests/unit/data/validators/TestValidator.php @@ -2,43 +2,42 @@ namespace yiiunit\data\validators; - use yii\validators\Validator; class TestValidator extends Validator { - private $_validatedAttributes = []; - private $_setErrorOnValidateAttribute = false; - - public function validateAttribute($object, $attribute) - { - $this->markAttributeValidated($attribute); - if ($this->_setErrorOnValidateAttribute == true) { - $this->addError($object, $attribute, sprintf('%s##%s', $attribute, get_class($object))); - } - } - - protected function markAttributeValidated($attr, $increaseBy = 1) - { - if (!isset($this->_validatedAttributes[$attr])) { - $this->_validatedAttributes[$attr] = 1; - } else { - $this->_validatedAttributes[$attr] = $this->_validatedAttributes[$attr] + $increaseBy; - } - } - - public function countAttributeValidations($attr) - { - return isset($this->_validatedAttributes[$attr]) ? $this->_validatedAttributes[$attr] : 0; - } - - public function isAttributeValidated($attr) - { - return isset($this->_validatedAttributes[$attr]); - } - - public function enableErrorOnValidateAttribute() - { - $this->_setErrorOnValidateAttribute = true; - } + private $_validatedAttributes = []; + private $_setErrorOnValidateAttribute = false; + + public function validateAttribute($object, $attribute) + { + $this->markAttributeValidated($attribute); + if ($this->_setErrorOnValidateAttribute == true) { + $this->addError($object, $attribute, sprintf('%s##%s', $attribute, get_class($object))); + } + } + + protected function markAttributeValidated($attr, $increaseBy = 1) + { + if (!isset($this->_validatedAttributes[$attr])) { + $this->_validatedAttributes[$attr] = 1; + } else { + $this->_validatedAttributes[$attr] = $this->_validatedAttributes[$attr] + $increaseBy; + } + } + + public function countAttributeValidations($attr) + { + return isset($this->_validatedAttributes[$attr]) ? $this->_validatedAttributes[$attr] : 0; + } + + public function isAttributeValidated($attr) + { + return isset($this->_validatedAttributes[$attr]); + } + + public function enableErrorOnValidateAttribute() + { + $this->_setErrorOnValidateAttribute = true; + } } diff --git a/tests/unit/data/validators/models/FakedValidationModel.php b/tests/unit/data/validators/models/FakedValidationModel.php index 92a31e794be..75d025c3850 100644 --- a/tests/unit/data/validators/models/FakedValidationModel.php +++ b/tests/unit/data/validators/models/FakedValidationModel.php @@ -6,60 +6,61 @@ class FakedValidationModel extends Model { - public $val_attr_a; - public $val_attr_b; - public $val_attr_c; - public $val_attr_d; - private $attr = []; + public $val_attr_a; + public $val_attr_b; + public $val_attr_c; + public $val_attr_d; + private $attr = []; - /** - * @param array $attributes - * @return self - */ - public static function createWithAttributes($attributes = []) - { - $m = new static(); - foreach ($attributes as $attribute => $value) { - $m->$attribute = $value; - } - return $m; - } + /** + * @param array $attributes + * @return self + */ + public static function createWithAttributes($attributes = []) + { + $m = new static(); + foreach ($attributes as $attribute => $value) { + $m->$attribute = $value; + } - public function rules() - { - return [ - [['val_attr_a', 'val_attr_b'], 'required', 'on' => 'reqTest'], - ['val_attr_c', 'integer'], - ['attr_images', 'file', 'maxFiles' => 3, 'types' => ['png'], 'on' => 'validateMultipleFiles'], - ['attr_image', 'file', 'types' => ['png'], 'on' => 'validateFile'] - ]; - } + return $m; + } - public function inlineVal($attribute, $params = []) - { - return true; - } + public function rules() + { + return [ + [['val_attr_a', 'val_attr_b'], 'required', 'on' => 'reqTest'], + ['val_attr_c', 'integer'], + ['attr_images', 'file', 'maxFiles' => 3, 'types' => ['png'], 'on' => 'validateMultipleFiles'], + ['attr_image', 'file', 'types' => ['png'], 'on' => 'validateFile'] + ]; + } - public function __get($name) - { - if (stripos($name, 'attr') === 0) { - return isset($this->attr[$name]) ? $this->attr[$name] : null; - } + public function inlineVal($attribute, $params = []) + { + return true; + } - return parent::__get($name); - } + public function __get($name) + { + if (stripos($name, 'attr') === 0) { + return isset($this->attr[$name]) ? $this->attr[$name] : null; + } - public function __set($name, $value) - { - if (stripos($name, 'attr') === 0) { - $this->attr[$name] = $value; - } else { - parent::__set($name, $value); - } - } + return parent::__get($name); + } - public function getAttributeLabel($attr) - { - return $attr; - } + public function __set($name, $value) + { + if (stripos($name, 'attr') === 0) { + $this->attr[$name] = $value; + } else { + parent::__set($name, $value); + } + } + + public function getAttributeLabel($attr) + { + return $attr; + } } diff --git a/tests/unit/data/validators/models/ValidatorTestMainModel.php b/tests/unit/data/validators/models/ValidatorTestMainModel.php index 665fcaac329..4cc8a54cc5c 100644 --- a/tests/unit/data/validators/models/ValidatorTestMainModel.php +++ b/tests/unit/data/validators/models/ValidatorTestMainModel.php @@ -2,20 +2,19 @@ namespace yiiunit\data\validators\models; - use yiiunit\data\ar\ActiveRecord; class ValidatorTestMainModel extends ActiveRecord { - public $testMainVal = 1; + public $testMainVal = 1; - public static function tableName() - { - return 'tbl_validator_main'; - } + public static function tableName() + { + return 'tbl_validator_main'; + } - public function getReferences() - { - return $this->hasMany(ValidatorTestRefModel::className(), ['ref' => 'id']); - } + public function getReferences() + { + return $this->hasMany(ValidatorTestRefModel::className(), ['ref' => 'id']); + } } diff --git a/tests/unit/data/validators/models/ValidatorTestRefModel.php b/tests/unit/data/validators/models/ValidatorTestRefModel.php index 3cbcf1ce86e..ab706ad6957 100644 --- a/tests/unit/data/validators/models/ValidatorTestRefModel.php +++ b/tests/unit/data/validators/models/ValidatorTestRefModel.php @@ -2,22 +2,21 @@ namespace yiiunit\data\validators\models; - use yiiunit\data\ar\ActiveRecord; class ValidatorTestRefModel extends ActiveRecord { - public $test_val = 2; - public $test_val_fail = 99; + public $test_val = 2; + public $test_val_fail = 99; - public static function tableName() - { - return 'tbl_validator_ref'; - } + public static function tableName() + { + return 'tbl_validator_ref'; + } - public function getMain() - { - return $this->hasOne(ValidatorTestMainModel::className(), ['id' => 'ref']); - } + public function getMain() + { + return $this->hasOne(ValidatorTestMainModel::className(), ['id' => 'ref']); + } } diff --git a/tests/unit/data/views/layout.php b/tests/unit/data/views/layout.php index 97a08888844..64eb18ef844 100644 --- a/tests/unit/data/views/layout.php +++ b/tests/unit/data/views/layout.php @@ -8,8 +8,8 @@ - Test - head(); ?> + Test + head(); ?> beginBody(); ?> diff --git a/tests/unit/data/views/simple.php b/tests/unit/data/views/simple.php index 437ba9035b3..25334861b3f 100644 --- a/tests/unit/data/views/simple.php +++ b/tests/unit/data/views/simple.php @@ -1 +1 @@ -This is a damn simple view file. \ No newline at end of file +This is a damn simple view file. diff --git a/tests/unit/extensions/authclient/AuthActionTest.php b/tests/unit/extensions/authclient/AuthActionTest.php index 45da52a17e8..0c0f6acffbb 100644 --- a/tests/unit/extensions/authclient/AuthActionTest.php +++ b/tests/unit/extensions/authclient/AuthActionTest.php @@ -2,67 +2,66 @@ namespace yiiunit\extensions\authclient; - use yii\authclient\AuthAction; class AuthActionTest extends TestCase { - protected function setUp() - { - $config = [ - 'components' => [ - 'user' => [ - 'identityClass' => '\yii\web\IdentityInterface' - ], - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); - } + protected function setUp() + { + $config = [ + 'components' => [ + 'user' => [ + 'identityClass' => '\yii\web\IdentityInterface' + ], + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } - public function testSetGet() - { - $action = new AuthAction(null, null); + public function testSetGet() + { + $action = new AuthAction(null, null); - $successUrl = 'http://test.success.url'; - $action->setSuccessUrl($successUrl); - $this->assertEquals($successUrl, $action->getSuccessUrl(), 'Unable to setup success URL!'); + $successUrl = 'http://test.success.url'; + $action->setSuccessUrl($successUrl); + $this->assertEquals($successUrl, $action->getSuccessUrl(), 'Unable to setup success URL!'); - $cancelUrl = 'http://test.cancel.url'; - $action->setCancelUrl($cancelUrl); - $this->assertEquals($cancelUrl, $action->getCancelUrl(), 'Unable to setup cancel URL!'); - } + $cancelUrl = 'http://test.cancel.url'; + $action->setCancelUrl($cancelUrl); + $this->assertEquals($cancelUrl, $action->getCancelUrl(), 'Unable to setup cancel URL!'); + } - /** - * @depends testSetGet - */ - public function testGetDefaultSuccessUrl() - { - $action = new AuthAction(null, null); + /** + * @depends testSetGet + */ + public function testGetDefaultSuccessUrl() + { + $action = new AuthAction(null, null); - $this->assertNotEmpty($action->getSuccessUrl(), 'Unable to get default success URL!'); - } + $this->assertNotEmpty($action->getSuccessUrl(), 'Unable to get default success URL!'); + } - /** - * @depends testSetGet - */ - public function testGetDefaultCancelUrl() - { - $action = new AuthAction(null, null); + /** + * @depends testSetGet + */ + public function testGetDefaultCancelUrl() + { + $action = new AuthAction(null, null); - $this->assertNotEmpty($action->getSuccessUrl(), 'Unable to get default cancel URL!'); - } + $this->assertNotEmpty($action->getSuccessUrl(), 'Unable to get default cancel URL!'); + } - public function testRedirect() - { - $action = new AuthAction(null, null); + public function testRedirect() + { + $action = new AuthAction(null, null); - $url = 'http://test.url'; - $response = $action->redirect($url, true); + $url = 'http://test.url'; + $response = $action->redirect($url, true); - $this->assertContains($url, $response->content); - } + $this->assertContains($url, $response->content); + } } diff --git a/tests/unit/extensions/authclient/BaseClientTest.php b/tests/unit/extensions/authclient/BaseClientTest.php index 3b3c3292efd..95bdcbdf1bd 100644 --- a/tests/unit/extensions/authclient/BaseClientTest.php +++ b/tests/unit/extensions/authclient/BaseClientTest.php @@ -6,75 +6,75 @@ class BaseClientTest extends TestCase { - public function testSetGet() - { - $client = new Client(); + public function testSetGet() + { + $client = new Client(); - $id = 'test_id'; - $client->setId($id); - $this->assertEquals($id, $client->getId(), 'Unable to setup id!'); + $id = 'test_id'; + $client->setId($id); + $this->assertEquals($id, $client->getId(), 'Unable to setup id!'); - $name = 'test_name'; - $client->setName($name); - $this->assertEquals($name, $client->getName(), 'Unable to setup name!'); + $name = 'test_name'; + $client->setName($name); + $this->assertEquals($name, $client->getName(), 'Unable to setup name!'); - $title = 'test_title'; - $client->setTitle($title); - $this->assertEquals($title, $client->getTitle(), 'Unable to setup title!'); + $title = 'test_title'; + $client->setTitle($title); + $this->assertEquals($title, $client->getTitle(), 'Unable to setup title!'); - $userAttributes = [ - 'attribute1' => 'value1', - 'attribute2' => 'value2', - ]; - $client->setUserAttributes($userAttributes); - $this->assertEquals($userAttributes, $client->getUserAttributes(), 'Unable to setup user attributes!'); + $userAttributes = [ + 'attribute1' => 'value1', + 'attribute2' => 'value2', + ]; + $client->setUserAttributes($userAttributes); + $this->assertEquals($userAttributes, $client->getUserAttributes(), 'Unable to setup user attributes!'); - $normalizeUserAttributeMap = [ - 'name' => 'some/name', - 'email' => 'some/email', - ]; - $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); - $this->assertEquals($normalizeUserAttributeMap, $client->getNormalizeUserAttributeMap(), 'Unable to setup normalize user attribute map!'); + $normalizeUserAttributeMap = [ + 'name' => 'some/name', + 'email' => 'some/email', + ]; + $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); + $this->assertEquals($normalizeUserAttributeMap, $client->getNormalizeUserAttributeMap(), 'Unable to setup normalize user attribute map!'); - $viewOptions = [ - 'option1' => 'value1', - 'option2' => 'value2', - ]; - $client->setViewOptions($viewOptions); - $this->assertEquals($viewOptions, $client->getViewOptions(), 'Unable to setup view options!'); - } + $viewOptions = [ + 'option1' => 'value1', + 'option2' => 'value2', + ]; + $client->setViewOptions($viewOptions); + $this->assertEquals($viewOptions, $client->getViewOptions(), 'Unable to setup view options!'); + } - public function testGetDefaults() - { - $client = new Client(); + public function testGetDefaults() + { + $client = new Client(); - $this->assertNotEmpty($client->getName(), 'Unable to get default name!'); - $this->assertNotEmpty($client->getTitle(), 'Unable to get default title!'); - $this->assertNotNull($client->getViewOptions(), 'Unable to get default view options!'); - $this->assertNotNull($client->getNormalizeUserAttributeMap(), 'Unable to get default normalize user attribute map!'); - } + $this->assertNotEmpty($client->getName(), 'Unable to get default name!'); + $this->assertNotEmpty($client->getTitle(), 'Unable to get default title!'); + $this->assertNotNull($client->getViewOptions(), 'Unable to get default view options!'); + $this->assertNotNull($client->getNormalizeUserAttributeMap(), 'Unable to get default normalize user attribute map!'); + } - /** - * @depends testSetGet - */ - public function testNormalizeUserAttributes() - { - $client = new Client(); + /** + * @depends testSetGet + */ + public function testNormalizeUserAttributes() + { + $client = new Client(); - $normalizeUserAttributeMap = [ - 'raw/name' => 'name', - 'raw/email' => 'email', - ]; - $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); - $rawUserAttributes = [ - 'raw/name' => 'name value', - 'raw/email' => 'email value', - ]; - $client->setUserAttributes($rawUserAttributes); - $normalizedUserAttributes = $client->getUserAttributes(); - $expectedNormalizedUserAttributes = array_combine(array_keys($normalizeUserAttributeMap), array_values($rawUserAttributes)); - $this->assertEquals($expectedNormalizedUserAttributes, $normalizedUserAttributes); - } + $normalizeUserAttributeMap = [ + 'raw/name' => 'name', + 'raw/email' => 'email', + ]; + $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); + $rawUserAttributes = [ + 'raw/name' => 'name value', + 'raw/email' => 'email value', + ]; + $client->setUserAttributes($rawUserAttributes); + $normalizedUserAttributes = $client->getUserAttributes(); + $expectedNormalizedUserAttributes = array_combine(array_keys($normalizeUserAttributeMap), array_values($rawUserAttributes)); + $this->assertEquals($expectedNormalizedUserAttributes, $normalizedUserAttributes); + } } class Client extends BaseClient diff --git a/tests/unit/extensions/authclient/BaseOAuthTest.php b/tests/unit/extensions/authclient/BaseOAuthTest.php index 025432c1624..ce293218f60 100644 --- a/tests/unit/extensions/authclient/BaseOAuthTest.php +++ b/tests/unit/extensions/authclient/BaseOAuthTest.php @@ -8,243 +8,245 @@ class BaseOAuthTest extends TestCase { - /** - * Creates test OAuth client instance. - * @return BaseOAuth oauth client. - */ - protected function createOAuthClient() - { - $oauthClient = $this->getMock(BaseOAuth::className(), ['setState', 'getState', 'composeRequestCurlOptions', 'refreshAccessToken', 'apiInternal']); - $oauthClient->expects($this->any())->method('setState')->will($this->returnValue($oauthClient)); - $oauthClient->expects($this->any())->method('getState')->will($this->returnValue(null)); - return $oauthClient; - } - - /** - * Invokes the OAuth client method even if it is protected. - * @param BaseOAuth $oauthClient OAuth client instance. - * @param string $methodName name of the method to be invoked. - * @param array $arguments method arguments. - * @return mixed method invoke result. - */ - protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) - { - $classReflection = new \ReflectionClass(get_class($oauthClient)); - $methodReflection = $classReflection->getMethod($methodName); - $methodReflection->setAccessible(true); - $result = $methodReflection->invokeArgs($oauthClient, $arguments); - $methodReflection->setAccessible(false); - return $result; - } - - // Tests : - - public function testSetGet() - { - $oauthClient = $this->createOAuthClient(); - - $returnUrl = 'http://test.return.url'; - $oauthClient->setReturnUrl($returnUrl); - $this->assertEquals($returnUrl, $oauthClient->getReturnUrl(), 'Unable to setup return URL!'); - - $curlOptions = [ - 'option1' => 'value1', - 'option2' => 'value2', - ]; - $oauthClient->setCurlOptions($curlOptions); - $this->assertEquals($curlOptions, $oauthClient->getCurlOptions(), 'Unable to setup cURL options!'); - } - - public function testSetupComponents() - { - $oauthClient = $this->createOAuthClient(); - - $oauthToken = new OAuthToken(); - $oauthClient->setAccessToken($oauthToken); - $this->assertEquals($oauthToken, $oauthClient->getAccessToken(), 'Unable to setup token!'); - - $oauthSignatureMethod = new PlainText(); - $oauthClient->setSignatureMethod($oauthSignatureMethod); - $this->assertEquals($oauthSignatureMethod, $oauthClient->getSignatureMethod(), 'Unable to setup signature method!'); - } - - /** - * @depends testSetupComponents - */ - public function testSetupComponentsByConfig() - { - $oauthClient = $this->createOAuthClient(); - - $oauthToken = [ - 'token' => 'test_token', - 'tokenSecret' => 'test_token_secret', - ]; - $oauthClient->setAccessToken($oauthToken); - $this->assertEquals($oauthToken['token'], $oauthClient->getAccessToken()->getToken(), 'Unable to setup token as config!'); - - $oauthSignatureMethod = [ - 'class' => 'yii\authclient\signature\PlainText' - ]; - $oauthClient->setSignatureMethod($oauthSignatureMethod); - $returnedSignatureMethod = $oauthClient->getSignatureMethod(); - $this->assertEquals($oauthSignatureMethod['class'], get_class($returnedSignatureMethod), 'Unable to setup signature method as config!'); - } - - /** - * Data provider for [[testComposeUrl()]]. - * @return array test data. - */ - public function composeUrlDataProvider() - { - return [ - [ - 'http://test.url', - [ - 'param1' => 'value1', - 'param2' => 'value2', - ], - 'http://test.url?param1=value1¶m2=value2', - ], - [ - 'http://test.url?with=some', - [ - 'param1' => 'value1', - 'param2' => 'value2', - ], - 'http://test.url?with=some¶m1=value1¶m2=value2', - ], - ]; - } - - /** - * @dataProvider composeUrlDataProvider - * - * @param string $url request URL. - * @param array $params request params - * @param string $expectedUrl expected composed URL. - */ - public function testComposeUrl($url, array $params, $expectedUrl) - { - $oauthClient = $this->createOAuthClient(); - $composedUrl = $this->invokeOAuthClientMethod($oauthClient, 'composeUrl', [$url, $params]); - $this->assertEquals($expectedUrl, $composedUrl); - } - - /** - * Data provider for {@link testDetermineContentTypeByHeaders}. - * @return array test data. - */ - public function determineContentTypeByHeadersDataProvider() - { - return [ - [ - ['content_type' => 'application/json'], - 'json' - ], - [ - ['content_type' => 'application/x-www-form-urlencoded'], - 'urlencoded' - ], - [ - ['content_type' => 'application/xml'], - 'xml' - ], - [ - ['some_header' => 'some_header_value'], - 'auto' - ], - [ - ['content_type' => 'unknown'], - 'auto' - ], - ]; - } - - /** - * @dataProvider determineContentTypeByHeadersDataProvider - * - * @param array $headers request headers. - * @param string $expectedResponseType expected response type. - */ - public function testDetermineContentTypeByHeaders(array $headers, $expectedResponseType) - { - $oauthClient = $this->createOAuthClient(); - $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByHeaders', [$headers]); - $this->assertEquals($expectedResponseType, $responseType); - } - - /** - * Data provider for [[testDetermineContentTypeByRaw]]. - * @return array test data. - */ - public function determineContentTypeByRawDataProvider() - { - return [ - ['{name: value}', 'json'], - ['name=value', 'urlencoded'], - ['name1=value1&name2=value2', 'urlencoded'], - ['Value', 'xml'], - ['Value', 'xml'], - ]; - } - - /** - * @dataProvider determineContentTypeByRawDataProvider - * - * @param string $rawResponse raw response content. - * @param string $expectedResponseType expected response type. - */ - public function testDetermineContentTypeByRaw($rawResponse, $expectedResponseType) - { - $oauthClient = $this->createOAuthClient(); - $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByRaw', [$rawResponse]); - $this->assertEquals($expectedResponseType, $responseType); - } - - /** - * Data provider for [[testApiUrl]]. - * @return array test data. - */ - public function apiUrlDataProvider() - { - return [ - [ - 'http://api.base.url', - 'sub/url', - 'http://api.base.url/sub/url', - ], - [ - 'http://api.base.url', - 'http://api.base.url/sub/url', - 'http://api.base.url/sub/url', - ], - [ - 'http://api.base.url', - 'https://api.base.url/sub/url', - 'https://api.base.url/sub/url', - ], - ]; - } - - /** - * @dataProvider apiUrlDataProvider - * - * @param $apiBaseUrl - * @param $apiSubUrl - * @param $expectedApiFullUrl - */ - public function testApiUrl($apiBaseUrl, $apiSubUrl, $expectedApiFullUrl) - { - $oauthClient = $this->createOAuthClient(); - $oauthClient->expects($this->any())->method('apiInternal')->will($this->returnArgument(1)); - - $accessToken = new OAuthToken(); - $accessToken->setToken('test_access_token'); - $accessToken->setExpireDuration(1000); - $oauthClient->setAccessToken($accessToken); - - $oauthClient->apiBaseUrl = $apiBaseUrl; - - $this->assertEquals($expectedApiFullUrl, $oauthClient->api($apiSubUrl)); - } + /** + * Creates test OAuth client instance. + * @return BaseOAuth oauth client. + */ + protected function createOAuthClient() + { + $oauthClient = $this->getMock(BaseOAuth::className(), ['setState', 'getState', 'composeRequestCurlOptions', 'refreshAccessToken', 'apiInternal']); + $oauthClient->expects($this->any())->method('setState')->will($this->returnValue($oauthClient)); + $oauthClient->expects($this->any())->method('getState')->will($this->returnValue(null)); + + return $oauthClient; + } + + /** + * Invokes the OAuth client method even if it is protected. + * @param BaseOAuth $oauthClient OAuth client instance. + * @param string $methodName name of the method to be invoked. + * @param array $arguments method arguments. + * @return mixed method invoke result. + */ + protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) + { + $classReflection = new \ReflectionClass(get_class($oauthClient)); + $methodReflection = $classReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($oauthClient, $arguments); + $methodReflection->setAccessible(false); + + return $result; + } + + // Tests : + + public function testSetGet() + { + $oauthClient = $this->createOAuthClient(); + + $returnUrl = 'http://test.return.url'; + $oauthClient->setReturnUrl($returnUrl); + $this->assertEquals($returnUrl, $oauthClient->getReturnUrl(), 'Unable to setup return URL!'); + + $curlOptions = [ + 'option1' => 'value1', + 'option2' => 'value2', + ]; + $oauthClient->setCurlOptions($curlOptions); + $this->assertEquals($curlOptions, $oauthClient->getCurlOptions(), 'Unable to setup cURL options!'); + } + + public function testSetupComponents() + { + $oauthClient = $this->createOAuthClient(); + + $oauthToken = new OAuthToken(); + $oauthClient->setAccessToken($oauthToken); + $this->assertEquals($oauthToken, $oauthClient->getAccessToken(), 'Unable to setup token!'); + + $oauthSignatureMethod = new PlainText(); + $oauthClient->setSignatureMethod($oauthSignatureMethod); + $this->assertEquals($oauthSignatureMethod, $oauthClient->getSignatureMethod(), 'Unable to setup signature method!'); + } + + /** + * @depends testSetupComponents + */ + public function testSetupComponentsByConfig() + { + $oauthClient = $this->createOAuthClient(); + + $oauthToken = [ + 'token' => 'test_token', + 'tokenSecret' => 'test_token_secret', + ]; + $oauthClient->setAccessToken($oauthToken); + $this->assertEquals($oauthToken['token'], $oauthClient->getAccessToken()->getToken(), 'Unable to setup token as config!'); + + $oauthSignatureMethod = [ + 'class' => 'yii\authclient\signature\PlainText' + ]; + $oauthClient->setSignatureMethod($oauthSignatureMethod); + $returnedSignatureMethod = $oauthClient->getSignatureMethod(); + $this->assertEquals($oauthSignatureMethod['class'], get_class($returnedSignatureMethod), 'Unable to setup signature method as config!'); + } + + /** + * Data provider for [[testComposeUrl()]]. + * @return array test data. + */ + public function composeUrlDataProvider() + { + return [ + [ + 'http://test.url', + [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + 'http://test.url?param1=value1¶m2=value2', + ], + [ + 'http://test.url?with=some', + [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + 'http://test.url?with=some¶m1=value1¶m2=value2', + ], + ]; + } + + /** + * @dataProvider composeUrlDataProvider + * + * @param string $url request URL. + * @param array $params request params + * @param string $expectedUrl expected composed URL. + */ + public function testComposeUrl($url, array $params, $expectedUrl) + { + $oauthClient = $this->createOAuthClient(); + $composedUrl = $this->invokeOAuthClientMethod($oauthClient, 'composeUrl', [$url, $params]); + $this->assertEquals($expectedUrl, $composedUrl); + } + + /** + * Data provider for {@link testDetermineContentTypeByHeaders}. + * @return array test data. + */ + public function determineContentTypeByHeadersDataProvider() + { + return [ + [ + ['content_type' => 'application/json'], + 'json' + ], + [ + ['content_type' => 'application/x-www-form-urlencoded'], + 'urlencoded' + ], + [ + ['content_type' => 'application/xml'], + 'xml' + ], + [ + ['some_header' => 'some_header_value'], + 'auto' + ], + [ + ['content_type' => 'unknown'], + 'auto' + ], + ]; + } + + /** + * @dataProvider determineContentTypeByHeadersDataProvider + * + * @param array $headers request headers. + * @param string $expectedResponseType expected response type. + */ + public function testDetermineContentTypeByHeaders(array $headers, $expectedResponseType) + { + $oauthClient = $this->createOAuthClient(); + $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByHeaders', [$headers]); + $this->assertEquals($expectedResponseType, $responseType); + } + + /** + * Data provider for [[testDetermineContentTypeByRaw]]. + * @return array test data. + */ + public function determineContentTypeByRawDataProvider() + { + return [ + ['{name: value}', 'json'], + ['name=value', 'urlencoded'], + ['name1=value1&name2=value2', 'urlencoded'], + ['Value', 'xml'], + ['Value', 'xml'], + ]; + } + + /** + * @dataProvider determineContentTypeByRawDataProvider + * + * @param string $rawResponse raw response content. + * @param string $expectedResponseType expected response type. + */ + public function testDetermineContentTypeByRaw($rawResponse, $expectedResponseType) + { + $oauthClient = $this->createOAuthClient(); + $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByRaw', [$rawResponse]); + $this->assertEquals($expectedResponseType, $responseType); + } + + /** + * Data provider for [[testApiUrl]]. + * @return array test data. + */ + public function apiUrlDataProvider() + { + return [ + [ + 'http://api.base.url', + 'sub/url', + 'http://api.base.url/sub/url', + ], + [ + 'http://api.base.url', + 'http://api.base.url/sub/url', + 'http://api.base.url/sub/url', + ], + [ + 'http://api.base.url', + 'https://api.base.url/sub/url', + 'https://api.base.url/sub/url', + ], + ]; + } + + /** + * @dataProvider apiUrlDataProvider + * + * @param $apiBaseUrl + * @param $apiSubUrl + * @param $expectedApiFullUrl + */ + public function testApiUrl($apiBaseUrl, $apiSubUrl, $expectedApiFullUrl) + { + $oauthClient = $this->createOAuthClient(); + $oauthClient->expects($this->any())->method('apiInternal')->will($this->returnArgument(1)); + + $accessToken = new OAuthToken(); + $accessToken->setToken('test_access_token'); + $accessToken->setExpireDuration(1000); + $oauthClient->setAccessToken($accessToken); + + $oauthClient->apiBaseUrl = $apiBaseUrl; + + $this->assertEquals($expectedApiFullUrl, $oauthClient->api($apiSubUrl)); + } } diff --git a/tests/unit/extensions/authclient/CollectionTest.php b/tests/unit/extensions/authclient/CollectionTest.php index c5a7add5bad..0aa70c16c65 100644 --- a/tests/unit/extensions/authclient/CollectionTest.php +++ b/tests/unit/extensions/authclient/CollectionTest.php @@ -7,76 +7,76 @@ class CollectionTest extends TestCase { - // Tests : + // Tests : - public function testSetGet() - { - $collection = new Collection(); + public function testSetGet() + { + $collection = new Collection(); - $clients = [ - 'testClient1' => new TestClient(), - 'testClient2' => new TestClient(), - ]; - $collection->setClients($clients); - $this->assertEquals($clients, $collection->getClients(), 'Unable to setup clients!'); - } + $clients = [ + 'testClient1' => new TestClient(), + 'testClient2' => new TestClient(), + ]; + $collection->setClients($clients); + $this->assertEquals($clients, $collection->getClients(), 'Unable to setup clients!'); + } - /** - * @depends testSetGet - */ - public function testGetProviderById() - { - $collection = new Collection(); + /** + * @depends testSetGet + */ + public function testGetProviderById() + { + $collection = new Collection(); - $clientId = 'testClientId'; - $client = new TestClient(); - $clients = [ - $clientId => $client - ]; - $collection->setClients($clients); + $clientId = 'testClientId'; + $client = new TestClient(); + $clients = [ + $clientId => $client + ]; + $collection->setClients($clients); - $this->assertEquals($client, $collection->getClient($clientId), 'Unable to get client by id!'); - } + $this->assertEquals($client, $collection->getClient($clientId), 'Unable to get client by id!'); + } - /** - * @depends testGetProviderById - */ - public function testCreateProvider() - { - $collection = new Collection(); + /** + * @depends testGetProviderById + */ + public function testCreateProvider() + { + $collection = new Collection(); - $clientId = 'testClientId'; - $clientClassName = TestClient::className(); - $clients = [ - $clientId => [ - 'class' => $clientClassName - ] - ]; - $collection->setClients($clients); + $clientId = 'testClientId'; + $clientClassName = TestClient::className(); + $clients = [ + $clientId => [ + 'class' => $clientClassName + ] + ]; + $collection->setClients($clients); - $provider = $collection->getClient($clientId); - $this->assertTrue(is_object($provider), 'Unable to create client by config!'); - $this->assertTrue(is_a($provider, $clientClassName), 'Client has wrong class name!'); - } + $provider = $collection->getClient($clientId); + $this->assertTrue(is_object($provider), 'Unable to create client by config!'); + $this->assertTrue(is_a($provider, $clientClassName), 'Client has wrong class name!'); + } - /** - * @depends testSetGet - */ - public function testHasProvider() - { - $collection = new Collection(); + /** + * @depends testSetGet + */ + public function testHasProvider() + { + $collection = new Collection(); - $clientName = 'testClientName'; - $clients = [ - $clientName => [ - 'class' => 'TestClient1' - ], - ]; - $collection->setClients($clients); + $clientName = 'testClientName'; + $clients = [ + $clientName => [ + 'class' => 'TestClient1' + ], + ]; + $collection->setClients($clients); - $this->assertTrue($collection->hasClient($clientName), 'Existing client check fails!'); - $this->assertFalse($collection->hasClient('unExistingClientName'), 'Not existing client check fails!'); - } + $this->assertTrue($collection->hasClient($clientName), 'Existing client check fails!'); + $this->assertFalse($collection->hasClient('unExistingClientName'), 'Not existing client check fails!'); + } } class TestClient extends BaseClient diff --git a/tests/unit/extensions/authclient/OAuth1Test.php b/tests/unit/extensions/authclient/OAuth1Test.php index 8b95f738c78..0a862adbb89 100644 --- a/tests/unit/extensions/authclient/OAuth1Test.php +++ b/tests/unit/extensions/authclient/OAuth1Test.php @@ -9,110 +9,111 @@ class OAuth1Test extends TestCase { - protected function setUp() - { - $config = [ - 'components' => [ - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); - } + protected function setUp() + { + $config = [ + 'components' => [ + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } - /** - * Invokes the OAuth client method even if it is protected. - * @param OAuth1 $oauthClient OAuth client instance. - * @param string $methodName name of the method to be invoked. - * @param array $arguments method arguments. - * @return mixed method invoke result. - */ - protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) - { - $classReflection = new \ReflectionClass(get_class($oauthClient)); - $methodReflection = $classReflection->getMethod($methodName); - $methodReflection->setAccessible(true); - $result = $methodReflection->invokeArgs($oauthClient, $arguments); - $methodReflection->setAccessible(false); - return $result; - } + /** + * Invokes the OAuth client method even if it is protected. + * @param OAuth1 $oauthClient OAuth client instance. + * @param string $methodName name of the method to be invoked. + * @param array $arguments method arguments. + * @return mixed method invoke result. + */ + protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) + { + $classReflection = new \ReflectionClass(get_class($oauthClient)); + $methodReflection = $classReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($oauthClient, $arguments); + $methodReflection->setAccessible(false); - // Tests : + return $result; + } - public function testSignRequest() - { - $oauthClient = new OAuth1(); + // Tests : - $oauthSignatureMethod = new PlainText(); - $oauthClient->setSignatureMethod($oauthSignatureMethod); + public function testSignRequest() + { + $oauthClient = new OAuth1(); - $signedParams = $this->invokeOAuthClientMethod($oauthClient, 'signRequest', ['GET', 'http://test.url', []]); - $this->assertNotEmpty($signedParams['oauth_signature'], 'Unable to sign request!'); - } + $oauthSignatureMethod = new PlainText(); + $oauthClient->setSignatureMethod($oauthSignatureMethod); - /** - * Data provider for [[testComposeAuthorizationHeader()]]. - * @return array test data. - */ - public function composeAuthorizationHeaderDataProvider() - { - return [ - [ - '', - [ - 'oauth_test_name_1' => 'oauth_test_value_1', - 'oauth_test_name_2' => 'oauth_test_value_2', - ], - 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' - ], - [ - 'test_realm', - [ - 'oauth_test_name_1' => 'oauth_test_value_1', - 'oauth_test_name_2' => 'oauth_test_value_2', - ], - 'Authorization: OAuth realm="test_realm", oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' - ], - [ - '', - [ - 'oauth_test_name_1' => 'oauth_test_value_1', - 'test_name_2' => 'test_value_2', - ], - 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1"' - ], - ]; - } + $signedParams = $this->invokeOAuthClientMethod($oauthClient, 'signRequest', ['GET', 'http://test.url', []]); + $this->assertNotEmpty($signedParams['oauth_signature'], 'Unable to sign request!'); + } - /** - * @dataProvider composeAuthorizationHeaderDataProvider - * - * @param string $realm authorization realm. - * @param array $params request params. - * @param string $expectedAuthorizationHeader expected authorization header. - */ - public function testComposeAuthorizationHeader($realm, array $params, $expectedAuthorizationHeader) - { - $oauthClient = new OAuth1(); - $authorizationHeader = $this->invokeOAuthClientMethod($oauthClient, 'composeAuthorizationHeader', [$params, $realm]); - $this->assertEquals($expectedAuthorizationHeader, $authorizationHeader); - } + /** + * Data provider for [[testComposeAuthorizationHeader()]]. + * @return array test data. + */ + public function composeAuthorizationHeaderDataProvider() + { + return [ + [ + '', + [ + 'oauth_test_name_1' => 'oauth_test_value_1', + 'oauth_test_name_2' => 'oauth_test_value_2', + ], + 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' + ], + [ + 'test_realm', + [ + 'oauth_test_name_1' => 'oauth_test_value_1', + 'oauth_test_name_2' => 'oauth_test_value_2', + ], + 'Authorization: OAuth realm="test_realm", oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' + ], + [ + '', + [ + 'oauth_test_name_1' => 'oauth_test_value_1', + 'test_name_2' => 'test_value_2', + ], + 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1"' + ], + ]; + } - public function testBuildAuthUrl() - { - $oauthClient = new OAuth1(); - $authUrl = 'http://test.auth.url'; - $oauthClient->authUrl = $authUrl; + /** + * @dataProvider composeAuthorizationHeaderDataProvider + * + * @param string $realm authorization realm. + * @param array $params request params. + * @param string $expectedAuthorizationHeader expected authorization header. + */ + public function testComposeAuthorizationHeader($realm, array $params, $expectedAuthorizationHeader) + { + $oauthClient = new OAuth1(); + $authorizationHeader = $this->invokeOAuthClientMethod($oauthClient, 'composeAuthorizationHeader', [$params, $realm]); + $this->assertEquals($expectedAuthorizationHeader, $authorizationHeader); + } - $requestTokenToken = 'test_request_token'; - $requestToken = new OAuthToken(); - $requestToken->setToken($requestTokenToken); + public function testBuildAuthUrl() + { + $oauthClient = new OAuth1(); + $authUrl = 'http://test.auth.url'; + $oauthClient->authUrl = $authUrl; - $builtAuthUrl = $oauthClient->buildAuthUrl($requestToken); + $requestTokenToken = 'test_request_token'; + $requestToken = new OAuthToken(); + $requestToken->setToken($requestTokenToken); - $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); - $this->assertContains($requestTokenToken, $builtAuthUrl, 'No token present!'); - } + $builtAuthUrl = $oauthClient->buildAuthUrl($requestToken); + + $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); + $this->assertContains($requestTokenToken, $builtAuthUrl, 'No token present!'); + } } diff --git a/tests/unit/extensions/authclient/OAuth2Test.php b/tests/unit/extensions/authclient/OAuth2Test.php index 8542eb46593..47e7b8b3b0d 100644 --- a/tests/unit/extensions/authclient/OAuth2Test.php +++ b/tests/unit/extensions/authclient/OAuth2Test.php @@ -7,35 +7,35 @@ class OAuth2Test extends TestCase { - protected function setUp() - { - $config = [ - 'components' => [ - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); - } + protected function setUp() + { + $config = [ + 'components' => [ + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } - // Tests : + // Tests : - public function testBuildAuthUrl() - { - $oauthClient = new OAuth2(); - $authUrl = 'http://test.auth.url'; - $oauthClient->authUrl = $authUrl; - $clientId = 'test_client_id'; - $oauthClient->clientId = $clientId; - $returnUrl = 'http://test.return.url'; - $oauthClient->setReturnUrl($returnUrl); + public function testBuildAuthUrl() + { + $oauthClient = new OAuth2(); + $authUrl = 'http://test.auth.url'; + $oauthClient->authUrl = $authUrl; + $clientId = 'test_client_id'; + $oauthClient->clientId = $clientId; + $returnUrl = 'http://test.return.url'; + $oauthClient->setReturnUrl($returnUrl); - $builtAuthUrl = $oauthClient->buildAuthUrl(); + $builtAuthUrl = $oauthClient->buildAuthUrl(); - $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); - $this->assertContains($clientId, $builtAuthUrl, 'No client id present!'); - $this->assertContains(rawurlencode($returnUrl), $builtAuthUrl, 'No return URL present!'); - } + $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); + $this->assertContains($clientId, $builtAuthUrl, 'No client id present!'); + $this->assertContains(rawurlencode($returnUrl), $builtAuthUrl, 'No return URL present!'); + } } diff --git a/tests/unit/extensions/authclient/OpenIdTest.php b/tests/unit/extensions/authclient/OpenIdTest.php index 097929666de..9a75bed3fb0 100644 --- a/tests/unit/extensions/authclient/OpenIdTest.php +++ b/tests/unit/extensions/authclient/OpenIdTest.php @@ -6,56 +6,56 @@ class OpenIdTest extends TestCase { - protected function setUp() - { - $config = [ - 'components' => [ - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); - } - - // Tests : - - public function testSetGet() - { - $client = new OpenId(); - - $trustRoot = 'http://trust.root'; - $client->setTrustRoot($trustRoot); - $this->assertEquals($trustRoot, $client->getTrustRoot(), 'Unable to setup trust root!'); - - $returnUrl = 'http://return.url'; - $client->setReturnUrl($returnUrl); - $this->assertEquals($returnUrl, $client->getReturnUrl(), 'Unable to setup return URL!'); - } - - /** - * @depends testSetGet - */ - public function testGetDefaults() - { - $client = new OpenId(); - - $this->assertNotEmpty($client->getTrustRoot(), 'Unable to get default trust root!'); - $this->assertNotEmpty($client->getReturnUrl(), 'Unable to get default return URL!'); - } - - public function testDiscover() - { - $url = 'https://www.google.com/accounts/o8/id'; - $client = new OpenId(); - $info = $client->discover($url); - $this->assertNotEmpty($info); - $this->assertNotEmpty($info['url']); - $this->assertNotEmpty($info['identity']); - $this->assertEquals(2, $info['version']); - $this->assertArrayHasKey('identifier_select', $info); - $this->assertArrayHasKey('ax', $info); - $this->assertArrayHasKey('sreg', $info); - } + protected function setUp() + { + $config = [ + 'components' => [ + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } + + // Tests : + + public function testSetGet() + { + $client = new OpenId(); + + $trustRoot = 'http://trust.root'; + $client->setTrustRoot($trustRoot); + $this->assertEquals($trustRoot, $client->getTrustRoot(), 'Unable to setup trust root!'); + + $returnUrl = 'http://return.url'; + $client->setReturnUrl($returnUrl); + $this->assertEquals($returnUrl, $client->getReturnUrl(), 'Unable to setup return URL!'); + } + + /** + * @depends testSetGet + */ + public function testGetDefaults() + { + $client = new OpenId(); + + $this->assertNotEmpty($client->getTrustRoot(), 'Unable to get default trust root!'); + $this->assertNotEmpty($client->getReturnUrl(), 'Unable to get default return URL!'); + } + + public function testDiscover() + { + $url = 'https://www.google.com/accounts/o8/id'; + $client = new OpenId(); + $info = $client->discover($url); + $this->assertNotEmpty($info); + $this->assertNotEmpty($info['url']); + $this->assertNotEmpty($info['identity']); + $this->assertEquals(2, $info['version']); + $this->assertArrayHasKey('identifier_select', $info); + $this->assertArrayHasKey('ax', $info); + $this->assertArrayHasKey('sreg', $info); + } } diff --git a/tests/unit/extensions/authclient/TestCase.php b/tests/unit/extensions/authclient/TestCase.php index 3b89f4f41c9..f08dfcc98ee 100644 --- a/tests/unit/extensions/authclient/TestCase.php +++ b/tests/unit/extensions/authclient/TestCase.php @@ -10,21 +10,21 @@ */ class TestCase extends \yiiunit\TestCase { - /** - * Adds sphinx extension files to [[Yii::$classPath]], - * avoiding the necessity of usage Composer autoloader. - */ - public static function loadClassMap() - { - $baseNameSpace = 'yii/authclient'; - $basePath = realpath(__DIR__. '/../../../../extensions/authclient'); - $files = FileHelper::findFiles($basePath); - foreach ($files as $file) { - $classRelativePath = str_replace($basePath, '', $file); - $classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath); - Yii::$classMap[$classFullName] = $file; - } - } + /** + * Adds sphinx extension files to [[Yii::$classPath]], + * avoiding the necessity of usage Composer autoloader. + */ + public static function loadClassMap() + { + $baseNameSpace = 'yii/authclient'; + $basePath = realpath(__DIR__. '/../../../../extensions/authclient'); + $files = FileHelper::findFiles($basePath); + foreach ($files as $file) { + $classRelativePath = str_replace($basePath, '', $file); + $classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath); + Yii::$classMap[$classFullName] = $file; + } + } } TestCase::loadClassMap(); diff --git a/tests/unit/extensions/authclient/TokenTest.php b/tests/unit/extensions/authclient/TokenTest.php index e173f7091d9..10387737780 100644 --- a/tests/unit/extensions/authclient/TokenTest.php +++ b/tests/unit/extensions/authclient/TokenTest.php @@ -7,127 +7,127 @@ class TokenTest extends TestCase { - public function testCreate() - { - $config = [ - 'tokenParamKey' => 'test_token_param_key', - 'tokenSecretParamKey' => 'test_token_secret_param_key', - ]; - $oauthToken = new OAuthToken($config); - $this->assertTrue(is_object($oauthToken), 'Unable to create access token!'); - foreach ($config as $name => $value) { - $this->assertEquals($value, $oauthToken->$name, 'Unable to setup attributes by constructor!'); - } - $this->assertTrue($oauthToken->createTimestamp > 0, 'Unable to fill create timestamp!'); - } - - public function testSetupParams() - { - $oauthToken = new OAuthToken(); - - $params = [ - 'name_1' => 'value_1', - 'name_2' => 'value_2', - ]; - $oauthToken->setParams($params); - $this->assertEquals($params, $oauthToken->getParams(), 'Unable to setup params!'); - - $newParamName = 'new_param_name'; - $newParamValue = 'new_param_value'; - $oauthToken->setParam($newParamName, $newParamValue); - $this->assertEquals($newParamValue, $oauthToken->getParam($newParamName), 'Unable to setup param by name!'); - } - - /** - * @depends testSetupParams - */ - public function testSetupParamsShortcuts() - { - $oauthToken = new OAuthToken(); - - $token = 'test_token_value'; - $oauthToken->setToken($token); - $this->assertEquals($token, $oauthToken->getToken(), 'Unable to setup token!'); - - $tokenSecret = 'test_token_secret'; - $oauthToken->setTokenSecret($tokenSecret); - $this->assertEquals($tokenSecret, $oauthToken->getTokenSecret(), 'Unable to setup token secret!'); - - $tokenExpireDuration = rand(1000, 2000); - $oauthToken->setExpireDuration($tokenExpireDuration); - $this->assertEquals($tokenExpireDuration, $oauthToken->getExpireDuration(), 'Unable to setup expire duration!'); - } - - /** - * Data provider for {@link testAutoFetchExpireDuration}. - * @return array test data. - */ - public function autoFetchExpireDurationDataProvider() - { - return [ - [ - ['expire_in' => 123345], - 123345 - ], - [ - ['expire' => 233456], - 233456 - ], - [ - ['expiry_in' => 34567], - 34567 - ], - [ - ['expiry' => 45678], - 45678 - ], - ]; - } - - /** - * @depends testSetupParamsShortcuts - * @dataProvider autoFetchExpireDurationDataProvider - * - * @param array $params - * @param $expectedExpireDuration - */ - public function testAutoFetchExpireDuration(array $params, $expectedExpireDuration) - { - $oauthToken = new OAuthToken(); - $oauthToken->setParams($params); - $this->assertEquals($expectedExpireDuration, $oauthToken->getExpireDuration()); - } - - /** - * @depends testSetupParamsShortcuts - */ - public function testGetIsExpired() - { - $oauthToken = new OAuthToken(); - $expireDuration = 3600; - $oauthToken->setExpireDuration($expireDuration); - - $this->assertFalse($oauthToken->getIsExpired(), 'Not expired token check fails!'); - - $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); - $this->assertTrue($oauthToken->getIsExpired(), 'Expired token check fails!'); - } - - /** - * @depends testGetIsExpired - */ - public function testGetIsValid() - { - $oauthToken = new OAuthToken(); - $expireDuration = 3600; - $oauthToken->setExpireDuration($expireDuration); - - $this->assertFalse($oauthToken->getIsValid(), 'Empty token is valid!'); - - $oauthToken->setToken('test_token'); - $this->assertTrue($oauthToken->getIsValid(), 'Filled up token is invalid!'); - - $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); - $this->assertFalse($oauthToken->getIsValid(), 'Expired token is valid!'); - } + public function testCreate() + { + $config = [ + 'tokenParamKey' => 'test_token_param_key', + 'tokenSecretParamKey' => 'test_token_secret_param_key', + ]; + $oauthToken = new OAuthToken($config); + $this->assertTrue(is_object($oauthToken), 'Unable to create access token!'); + foreach ($config as $name => $value) { + $this->assertEquals($value, $oauthToken->$name, 'Unable to setup attributes by constructor!'); + } + $this->assertTrue($oauthToken->createTimestamp > 0, 'Unable to fill create timestamp!'); + } + + public function testSetupParams() + { + $oauthToken = new OAuthToken(); + + $params = [ + 'name_1' => 'value_1', + 'name_2' => 'value_2', + ]; + $oauthToken->setParams($params); + $this->assertEquals($params, $oauthToken->getParams(), 'Unable to setup params!'); + + $newParamName = 'new_param_name'; + $newParamValue = 'new_param_value'; + $oauthToken->setParam($newParamName, $newParamValue); + $this->assertEquals($newParamValue, $oauthToken->getParam($newParamName), 'Unable to setup param by name!'); + } + + /** + * @depends testSetupParams + */ + public function testSetupParamsShortcuts() + { + $oauthToken = new OAuthToken(); + + $token = 'test_token_value'; + $oauthToken->setToken($token); + $this->assertEquals($token, $oauthToken->getToken(), 'Unable to setup token!'); + + $tokenSecret = 'test_token_secret'; + $oauthToken->setTokenSecret($tokenSecret); + $this->assertEquals($tokenSecret, $oauthToken->getTokenSecret(), 'Unable to setup token secret!'); + + $tokenExpireDuration = rand(1000, 2000); + $oauthToken->setExpireDuration($tokenExpireDuration); + $this->assertEquals($tokenExpireDuration, $oauthToken->getExpireDuration(), 'Unable to setup expire duration!'); + } + + /** + * Data provider for {@link testAutoFetchExpireDuration}. + * @return array test data. + */ + public function autoFetchExpireDurationDataProvider() + { + return [ + [ + ['expire_in' => 123345], + 123345 + ], + [ + ['expire' => 233456], + 233456 + ], + [ + ['expiry_in' => 34567], + 34567 + ], + [ + ['expiry' => 45678], + 45678 + ], + ]; + } + + /** + * @depends testSetupParamsShortcuts + * @dataProvider autoFetchExpireDurationDataProvider + * + * @param array $params + * @param $expectedExpireDuration + */ + public function testAutoFetchExpireDuration(array $params, $expectedExpireDuration) + { + $oauthToken = new OAuthToken(); + $oauthToken->setParams($params); + $this->assertEquals($expectedExpireDuration, $oauthToken->getExpireDuration()); + } + + /** + * @depends testSetupParamsShortcuts + */ + public function testGetIsExpired() + { + $oauthToken = new OAuthToken(); + $expireDuration = 3600; + $oauthToken->setExpireDuration($expireDuration); + + $this->assertFalse($oauthToken->getIsExpired(), 'Not expired token check fails!'); + + $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); + $this->assertTrue($oauthToken->getIsExpired(), 'Expired token check fails!'); + } + + /** + * @depends testGetIsExpired + */ + public function testGetIsValid() + { + $oauthToken = new OAuthToken(); + $expireDuration = 3600; + $oauthToken->setExpireDuration($expireDuration); + + $this->assertFalse($oauthToken->getIsValid(), 'Empty token is valid!'); + + $oauthToken->setToken('test_token'); + $this->assertTrue($oauthToken->getIsValid(), 'Filled up token is invalid!'); + + $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); + $this->assertFalse($oauthToken->getIsValid(), 'Expired token is valid!'); + } } diff --git a/tests/unit/extensions/authclient/signature/BaseMethodTest.php b/tests/unit/extensions/authclient/signature/BaseMethodTest.php index fd645fbc6cb..aa838ff5d7f 100644 --- a/tests/unit/extensions/authclient/signature/BaseMethodTest.php +++ b/tests/unit/extensions/authclient/signature/BaseMethodTest.php @@ -6,45 +6,46 @@ class BaseMethodTest extends TestCase { - /** - * Creates test signature method instance. - * @return \yii\authclient\signature\BaseMethod - */ - protected function createTestSignatureMethod() - { - $signatureMethod = $this->getMock('\yii\authclient\signature\BaseMethod', ['getName', 'generateSignature']); - $signatureMethod->expects($this->any())->method('getName')->will($this->returnValue('testMethodName')); - $signatureMethod->expects($this->any())->method('generateSignature')->will($this->returnValue('testSignature')); - return $signatureMethod; - } - - // Tests : - - public function testGenerateSignature() - { - $signatureMethod = $this->createTestSignatureMethod(); - - $baseString = 'test_base_string'; - $key = 'test_key'; - - $signature = $signatureMethod->generateSignature($baseString, $key); - - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } - - /** - * @depends testGenerateSignature - */ - public function testVerify() - { - $signatureMethod = $this->createTestSignatureMethod(); - - $baseString = 'test_base_string'; - $key = 'test_key'; - $signature = 'unsigned'; - $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); - - $generatedSignature = $signatureMethod->generateSignature($baseString, $key); - $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); - } + /** + * Creates test signature method instance. + * @return \yii\authclient\signature\BaseMethod + */ + protected function createTestSignatureMethod() + { + $signatureMethod = $this->getMock('\yii\authclient\signature\BaseMethod', ['getName', 'generateSignature']); + $signatureMethod->expects($this->any())->method('getName')->will($this->returnValue('testMethodName')); + $signatureMethod->expects($this->any())->method('generateSignature')->will($this->returnValue('testSignature')); + + return $signatureMethod; + } + + // Tests : + + public function testGenerateSignature() + { + $signatureMethod = $this->createTestSignatureMethod(); + + $baseString = 'test_base_string'; + $key = 'test_key'; + + $signature = $signatureMethod->generateSignature($baseString, $key); + + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } + + /** + * @depends testGenerateSignature + */ + public function testVerify() + { + $signatureMethod = $this->createTestSignatureMethod(); + + $baseString = 'test_base_string'; + $key = 'test_key'; + $signature = 'unsigned'; + $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); + + $generatedSignature = $signatureMethod->generateSignature($baseString, $key); + $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); + } } diff --git a/tests/unit/extensions/authclient/signature/HmacSha1Test.php b/tests/unit/extensions/authclient/signature/HmacSha1Test.php index 8bbc91138ba..e766793d973 100644 --- a/tests/unit/extensions/authclient/signature/HmacSha1Test.php +++ b/tests/unit/extensions/authclient/signature/HmacSha1Test.php @@ -7,14 +7,14 @@ class HmacSha1Test extends TestCase { - public function testGenerateSignature() - { - $signatureMethod = new HmacSha1(); + public function testGenerateSignature() + { + $signatureMethod = new HmacSha1(); - $baseString = 'test_base_string'; - $key = 'test_key'; + $baseString = 'test_base_string'; + $key = 'test_key'; - $signature = $signatureMethod->generateSignature($baseString, $key); - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } + $signature = $signatureMethod->generateSignature($baseString, $key); + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } } diff --git a/tests/unit/extensions/authclient/signature/PlainTextTest.php b/tests/unit/extensions/authclient/signature/PlainTextTest.php index 80d0030819c..584cff65acd 100644 --- a/tests/unit/extensions/authclient/signature/PlainTextTest.php +++ b/tests/unit/extensions/authclient/signature/PlainTextTest.php @@ -7,14 +7,14 @@ class PlainTextTest extends TestCase { - public function testGenerateSignature() - { - $signatureMethod = new PlainText(); + public function testGenerateSignature() + { + $signatureMethod = new PlainText(); - $baseString = 'test_base_string'; - $key = 'test_key'; + $baseString = 'test_base_string'; + $key = 'test_key'; - $signature = $signatureMethod->generateSignature($baseString, $key); - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } + $signature = $signatureMethod->generateSignature($baseString, $key); + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } } diff --git a/tests/unit/extensions/authclient/signature/RsaSha1Test.php b/tests/unit/extensions/authclient/signature/RsaSha1Test.php index e63ce23e88d..447c3cd160a 100644 --- a/tests/unit/extensions/authclient/signature/RsaSha1Test.php +++ b/tests/unit/extensions/authclient/signature/RsaSha1Test.php @@ -7,13 +7,13 @@ class RsaSha1Test extends TestCase { - /** - * Returns test public certificate string. - * @return string public certificate string. - */ - protected function getTestPublicCertificate() - { - return '-----BEGIN CERTIFICATE----- + /** + * Returns test public certificate string. + * @return string public certificate string. + */ + protected function getTestPublicCertificate() + { + return '-----BEGIN CERTIFICATE----- MIIDJDCCAo2gAwIBAgIJALCFAl3nj1ibMA0GCSqGSIb3DQEBBQUAMIGqMQswCQYD VQQGEwJOTDESMBAGA1UECAwJQW1zdGVyZGFtMRIwEAYDVQQHDAlBbXN0ZXJkYW0x DzANBgNVBAoMBlBpbVRpbTEPMA0GA1UECwwGUGltVGltMSswKQYDVQQDDCJkZXY1 @@ -32,15 +32,15 @@ protected function getTestPublicCertificate() DiiFw5gDrc6Qp6WtPmVhxHUWl6O5bOG8lG0Dcppeed9454CGvBShmYdwC6vk0s7/ gVdK2V4fYsUeT6u49ONshvJ/8xhHz2gGXeLWaqHwtK3Dl3S6TIDuoQ== -----END CERTIFICATE-----'; - } + } - /** - * Returns test private certificate string. - * @return string private certificate string. - */ - protected function getTestPrivateCertificate() - { - return '-----BEGIN RSA PRIVATE KEY----- + /** + * Returns test private certificate string. + * @return string private certificate string. + */ + protected function getTestPrivateCertificate() + { + return '-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDE0d63YwpBLxzxQAW887JALcGruAHkHu7Ui1oc7bCIMy+ud6rP gNmbFLw3GoGzQ8xhMmksZHsS07IfWRTDeisPHAqfgcApOZbyMyZUAL6+1ko4xAIP nQSia7l8M4nWgtgqifDCbFKAoPXuWSrYDOFtgSkBLH5xYyFPRc04nnHpoQIDAQAB @@ -55,56 +55,56 @@ protected function getTestPrivateCertificate() IyvuagHJR379p4dePwJBAMCkYSATGdhYbeDfySWUro5K0QAvBNj8FuNJQ4rqUxz8 8b+OXIyd5WlmuDRTDGJBTxAYeaioTuMCFWaZm4jG0I4= -----END RSA PRIVATE KEY-----'; - } + } - // Tests : + // Tests : - public function testGenerateSignature() - { - $signatureMethod = new RsaSha1(); - $signatureMethod->setPrivateCertificate($this->getTestPrivateCertificate()); - $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); + public function testGenerateSignature() + { + $signatureMethod = new RsaSha1(); + $signatureMethod->setPrivateCertificate($this->getTestPrivateCertificate()); + $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); - $baseString = 'test_base_string'; - $key = 'test_key'; + $baseString = 'test_base_string'; + $key = 'test_key'; - $signature = $signatureMethod->generateSignature($baseString, $key); - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } + $signature = $signatureMethod->generateSignature($baseString, $key); + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } - /** - * @depends testGenerateSignature - */ - public function testVerify() - { - $signatureMethod = new RsaSha1(); - $signatureMethod->setPrivateCertificate($this->getTestPrivateCertificate()); - $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); + /** + * @depends testGenerateSignature + */ + public function testVerify() + { + $signatureMethod = new RsaSha1(); + $signatureMethod->setPrivateCertificate($this->getTestPrivateCertificate()); + $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); - $baseString = 'test_base_string'; - $key = 'test_key'; - $signature = 'unsigned'; - $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); + $baseString = 'test_base_string'; + $key = 'test_key'; + $signature = 'unsigned'; + $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); - $generatedSignature = $signatureMethod->generateSignature($baseString, $key); - $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); - } + $generatedSignature = $signatureMethod->generateSignature($baseString, $key); + $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); + } - public function testInitPrivateCertificate() - { - $signatureMethod = new RsaSha1(); + public function testInitPrivateCertificate() + { + $signatureMethod = new RsaSha1(); - $certificateFileName = __FILE__; - $signatureMethod->privateCertificateFile = $certificateFileName; - $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPrivateCertificate(), 'Unable to fetch private certificate from file!'); - } + $certificateFileName = __FILE__; + $signatureMethod->privateCertificateFile = $certificateFileName; + $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPrivateCertificate(), 'Unable to fetch private certificate from file!'); + } - public function testInitPublicCertificate() - { - $signatureMethod = new RsaSha1(); + public function testInitPublicCertificate() + { + $signatureMethod = new RsaSha1(); - $certificateFileName = __FILE__; - $signatureMethod->publicCertificateFile = $certificateFileName; - $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPublicCertificate(), 'Unable to fetch public certificate from file!'); - } + $certificateFileName = __FILE__; + $signatureMethod->publicCertificateFile = $certificateFileName; + $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPublicCertificate(), 'Unable to fetch public certificate from file!'); + } } diff --git a/tests/unit/extensions/elasticsearch/ActiveRecordTest.php b/tests/unit/extensions/elasticsearch/ActiveRecordTest.php index ca3158c47cf..ef8889c5a7e 100644 --- a/tests/unit/extensions/elasticsearch/ActiveRecordTest.php +++ b/tests/unit/extensions/elasticsearch/ActiveRecordTest.php @@ -18,540 +18,540 @@ */ class ActiveRecordTest extends ElasticSearchTestCase { - use ActiveRecordTestTrait; - - public function callCustomerFind($q = null) - { - return Customer::find($q); - } - - public function callOrderFind($q = null) - { - return Order::find($q); - } - - public function callOrderItemFind($q = null) - { - return OrderItem::find($q); - } - - public function callItemFind($q = null) - { - return Item::find($q); - } - - public function getCustomerClass() - { - return Customer::className(); - } - - public function getItemClass() - { - return Item::className(); - } - - public function getOrderClass() - { - return Order::className(); - } - - public function getOrderItemClass() - { - return OrderItem::className(); - } - - /** - * can be overridden to do things after save() - */ - public function afterSave() - { - $this->getConnection()->createCommand()->flushIndex('yiitest'); - } - - public function setUp() - { - parent::setUp(); - - /** @var Connection $db */ - $db = ActiveRecord::$db = $this->getConnection(); - - // delete index - if ($db->createCommand()->indexExists('yiitest')) { - $db->createCommand()->deleteIndex('yiitest'); - } - $db->createCommand()->createIndex('yiitest'); - - $command = $db->createCommand(); - Customer::setUpMapping($command); - Item::setUpMapping($command); - Order::setUpMapping($command); - OrderItem::setUpMapping($command); - - $db->createCommand()->flushIndex('yiitest'); - - $customer = new Customer(); - $customer->id = 1; - $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->id = 2; - $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->id = 3; - $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); - $customer->save(false); + use ActiveRecordTestTrait; + + public function callCustomerFind($q = null) + { + return Customer::find($q); + } + + public function callOrderFind($q = null) + { + return Order::find($q); + } + + public function callOrderItemFind($q = null) + { + return OrderItem::find($q); + } + + public function callItemFind($q = null) + { + return Item::find($q); + } + + public function getCustomerClass() + { + return Customer::className(); + } + + public function getItemClass() + { + return Item::className(); + } + + public function getOrderClass() + { + return Order::className(); + } + + public function getOrderItemClass() + { + return OrderItem::className(); + } + + /** + * can be overridden to do things after save() + */ + public function afterSave() + { + $this->getConnection()->createCommand()->flushIndex('yiitest'); + } + + public function setUp() + { + parent::setUp(); + + /** @var Connection $db */ + $db = ActiveRecord::$db = $this->getConnection(); + + // delete index + if ($db->createCommand()->indexExists('yiitest')) { + $db->createCommand()->deleteIndex('yiitest'); + } + $db->createCommand()->createIndex('yiitest'); + + $command = $db->createCommand(); + Customer::setUpMapping($command); + Item::setUpMapping($command); + Order::setUpMapping($command); + OrderItem::setUpMapping($command); + + $db->createCommand()->flushIndex('yiitest'); + + $customer = new Customer(); + $customer->id = 1; + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->id = 2; + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->id = 3; + $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); + $customer->save(false); // INSERT INTO tbl_category (name) VALUES ('Books'); // INSERT INTO tbl_category (name) VALUES ('Movies'); - $item = new Item(); - $item->id = 1; - $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->id = 2; - $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->id = 3; - $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->id = 4; - $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->id = 5; - $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); - $item->save(false); - - $order = new Order(); - $order->id = 1; - $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); - $order->save(false); - $order = new Order(); - $order->id = 2; - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); - $order->save(false); - $order = new Order(); - $order->id = 3; - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); - $order->save(false); - - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); - $orderItem->save(false); - - $db->createCommand()->flushIndex('yiitest'); - } - - public function testFindAsArray() - { - // asArray - $customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one(); - $this->assertEquals([ - 'id' => 2, - 'email' => 'user2@example.com', - 'name' => 'user2', - 'address' => 'address2', - 'status' => 1, - '_score' => 1.0 - ], $customer); - } - - public function testSearch() - { - $customers = $this->callCustomerFind()->search()['hits']; - $this->assertEquals(3, $customers['total']); - $this->assertEquals(3, count($customers['hits'])); - $this->assertTrue($customers['hits'][0] instanceof Customer); - $this->assertTrue($customers['hits'][1] instanceof Customer); - $this->assertTrue($customers['hits'][2] instanceof Customer); - - // limit vs. totalcount - $customers = $this->callCustomerFind()->limit(2)->search()['hits']; - $this->assertEquals(3, $customers['total']); - $this->assertEquals(2, count($customers['hits'])); - - // asArray - $result = $this->callCustomerFind()->asArray()->search()['hits']; - $this->assertEquals(3, $result['total']); - $customers = $result['hits']; - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers[0]); - $this->assertArrayHasKey('name', $customers[0]); - $this->assertArrayHasKey('email', $customers[0]); - $this->assertArrayHasKey('address', $customers[0]); - $this->assertArrayHasKey('status', $customers[0]); - $this->assertArrayHasKey('id', $customers[1]); - $this->assertArrayHasKey('name', $customers[1]); - $this->assertArrayHasKey('email', $customers[1]); - $this->assertArrayHasKey('address', $customers[1]); - $this->assertArrayHasKey('status', $customers[1]); - $this->assertArrayHasKey('id', $customers[2]); - $this->assertArrayHasKey('name', $customers[2]); - $this->assertArrayHasKey('email', $customers[2]); - $this->assertArrayHasKey('address', $customers[2]); - $this->assertArrayHasKey('status', $customers[2]); - - // TODO test asArray() + fields() + indexBy() - - // find by attributes - $result = $this->callCustomerFind()->where(['name' => 'user2'])->search()['hits']; - $customer = reset($result['hits']); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(2, $customer->id); - - // TODO test query() and filter() - } - - public function testSearchFacets() - { - $result = $this->callCustomerFind()->addStatisticalFacet('status_stats', ['field' => 'status'])->search(); - $this->assertArrayHasKey('facets', $result); - $this->assertEquals(3, $result['facets']['status_stats']['count']); - $this->assertEquals(4, $result['facets']['status_stats']['total']); // sum of values - $this->assertEquals(1, $result['facets']['status_stats']['min']); - $this->assertEquals(2, $result['facets']['status_stats']['max']); - } - - public function testGetDb() - { - $this->mockApplication(['components' => ['elasticsearch' => Connection::className()]]); - $this->assertInstanceOf(Connection::className(), ActiveRecord::getDb()); - } - - public function testGet() - { - $this->assertInstanceOf(Customer::className(), Customer::get(1)); - $this->assertNull(Customer::get(5)); - } - - public function testMget() - { - $this->assertEquals([], Customer::mget([])); - - $records = Customer::mget([1]); - $this->assertEquals(1, count($records)); - $this->assertInstanceOf(Customer::className(), reset($records)); - - $records = Customer::mget([5]); - $this->assertEquals(0, count($records)); - - $records = Customer::mget([1, 3, 5]); - $this->assertEquals(2, count($records)); - $this->assertInstanceOf(Customer::className(), $records[0]); - $this->assertInstanceOf(Customer::className(), $records[1]); - } - - public function testFindLazy() - { - /** @var $customer Customer */ - $customer = Customer::find(2); - $orders = $customer->orders; - $this->assertEquals(2, count($orders)); - - $orders = $customer->getOrders()->where(['between', 'created_at', 1325334000, 1325400000])->all(); - $this->assertEquals(1, count($orders)); - $this->assertEquals(2, $orders[0]->id); - } - - public function testFindEagerViaRelation() - { - $orders = Order::find()->with('items')->orderBy('created_at')->all(); - $this->assertEquals(3, count($orders)); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertTrue($order->isRelationPopulated('items')); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testInsertNoPk() - { - $this->assertEquals(['id'], Customer::primaryKey()); - $pkName = 'id'; - - $customer = new Customer; - $customer->email = 'user4@example.com'; - $customer->name = 'user4'; - $customer->address = 'address4'; - - $this->assertNull($customer->primaryKey); - $this->assertNull($customer->oldPrimaryKey); - $this->assertNull($customer->$pkName); - $this->assertTrue($customer->isNewRecord); - - $customer->save(); - $this->afterSave(); - - $this->assertNotNull($customer->primaryKey); - $this->assertNotNull($customer->oldPrimaryKey); - $this->assertNotNull($customer->$pkName); - $this->assertEquals($customer->primaryKey, $customer->oldPrimaryKey); - $this->assertEquals($customer->primaryKey, $customer->$pkName); - $this->assertFalse($customer->isNewRecord); - } - - public function testInsertPk() - { - $pkName = 'id'; - - $customer = new Customer; - $customer->$pkName = 5; - $customer->email = 'user5@example.com'; - $customer->name = 'user5'; - $customer->address = 'address5'; - - $this->assertTrue($customer->isNewRecord); - - $customer->save(); - - $this->assertEquals(5, $customer->primaryKey); - $this->assertEquals(5, $customer->oldPrimaryKey); - $this->assertEquals(5, $customer->$pkName); - $this->assertFalse($customer->isNewRecord); - } - - public function testUpdatePk() - { - $pkName = 'id'; - - $orderItem = Order::find([$pkName => 2]); - $this->assertEquals(2, $orderItem->primaryKey); - $this->assertEquals(2, $orderItem->oldPrimaryKey); - $this->assertEquals(2, $orderItem->$pkName); + $item = new Item(); + $item->id = 1; + $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->id = 2; + $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->id = 3; + $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->id = 4; + $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->id = 5; + $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); + $item->save(false); + + $order = new Order(); + $order->id = 1; + $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new Order(); + $order->id = 2; + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new Order(); + $order->id = 3; + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + + $db->createCommand()->flushIndex('yiitest'); + } + + public function testFindAsArray() + { + // asArray + $customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one(); + $this->assertEquals([ + 'id' => 2, + 'email' => 'user2@example.com', + 'name' => 'user2', + 'address' => 'address2', + 'status' => 1, + '_score' => 1.0 + ], $customer); + } + + public function testSearch() + { + $customers = $this->callCustomerFind()->search()['hits']; + $this->assertEquals(3, $customers['total']); + $this->assertEquals(3, count($customers['hits'])); + $this->assertTrue($customers['hits'][0] instanceof Customer); + $this->assertTrue($customers['hits'][1] instanceof Customer); + $this->assertTrue($customers['hits'][2] instanceof Customer); + + // limit vs. totalcount + $customers = $this->callCustomerFind()->limit(2)->search()['hits']; + $this->assertEquals(3, $customers['total']); + $this->assertEquals(2, count($customers['hits'])); + + // asArray + $result = $this->callCustomerFind()->asArray()->search()['hits']; + $this->assertEquals(3, $result['total']); + $customers = $result['hits']; + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers[0]); + $this->assertArrayHasKey('name', $customers[0]); + $this->assertArrayHasKey('email', $customers[0]); + $this->assertArrayHasKey('address', $customers[0]); + $this->assertArrayHasKey('status', $customers[0]); + $this->assertArrayHasKey('id', $customers[1]); + $this->assertArrayHasKey('name', $customers[1]); + $this->assertArrayHasKey('email', $customers[1]); + $this->assertArrayHasKey('address', $customers[1]); + $this->assertArrayHasKey('status', $customers[1]); + $this->assertArrayHasKey('id', $customers[2]); + $this->assertArrayHasKey('name', $customers[2]); + $this->assertArrayHasKey('email', $customers[2]); + $this->assertArrayHasKey('address', $customers[2]); + $this->assertArrayHasKey('status', $customers[2]); + + // TODO test asArray() + fields() + indexBy() + + // find by attributes + $result = $this->callCustomerFind()->where(['name' => 'user2'])->search()['hits']; + $customer = reset($result['hits']); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals(2, $customer->id); + + // TODO test query() and filter() + } + + public function testSearchFacets() + { + $result = $this->callCustomerFind()->addStatisticalFacet('status_stats', ['field' => 'status'])->search(); + $this->assertArrayHasKey('facets', $result); + $this->assertEquals(3, $result['facets']['status_stats']['count']); + $this->assertEquals(4, $result['facets']['status_stats']['total']); // sum of values + $this->assertEquals(1, $result['facets']['status_stats']['min']); + $this->assertEquals(2, $result['facets']['status_stats']['max']); + } + + public function testGetDb() + { + $this->mockApplication(['components' => ['elasticsearch' => Connection::className()]]); + $this->assertInstanceOf(Connection::className(), ActiveRecord::getDb()); + } + + public function testGet() + { + $this->assertInstanceOf(Customer::className(), Customer::get(1)); + $this->assertNull(Customer::get(5)); + } + + public function testMget() + { + $this->assertEquals([], Customer::mget([])); + + $records = Customer::mget([1]); + $this->assertEquals(1, count($records)); + $this->assertInstanceOf(Customer::className(), reset($records)); + + $records = Customer::mget([5]); + $this->assertEquals(0, count($records)); + + $records = Customer::mget([1, 3, 5]); + $this->assertEquals(2, count($records)); + $this->assertInstanceOf(Customer::className(), $records[0]); + $this->assertInstanceOf(Customer::className(), $records[1]); + } + + public function testFindLazy() + { + /** @var $customer Customer */ + $customer = Customer::find(2); + $orders = $customer->orders; + $this->assertEquals(2, count($orders)); + + $orders = $customer->getOrders()->where(['between', 'created_at', 1325334000, 1325400000])->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(2, $orders[0]->id); + } + + public function testFindEagerViaRelation() + { + $orders = Order::find()->with('items')->orderBy('created_at')->all(); + $this->assertEquals(3, count($orders)); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('items')); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testInsertNoPk() + { + $this->assertEquals(['id'], Customer::primaryKey()); + $pkName = 'id'; + + $customer = new Customer; + $customer->email = 'user4@example.com'; + $customer->name = 'user4'; + $customer->address = 'address4'; + + $this->assertNull($customer->primaryKey); + $this->assertNull($customer->oldPrimaryKey); + $this->assertNull($customer->$pkName); + $this->assertTrue($customer->isNewRecord); + + $customer->save(); + $this->afterSave(); + + $this->assertNotNull($customer->primaryKey); + $this->assertNotNull($customer->oldPrimaryKey); + $this->assertNotNull($customer->$pkName); + $this->assertEquals($customer->primaryKey, $customer->oldPrimaryKey); + $this->assertEquals($customer->primaryKey, $customer->$pkName); + $this->assertFalse($customer->isNewRecord); + } + + public function testInsertPk() + { + $pkName = 'id'; + + $customer = new Customer; + $customer->$pkName = 5; + $customer->email = 'user5@example.com'; + $customer->name = 'user5'; + $customer->address = 'address5'; + + $this->assertTrue($customer->isNewRecord); + + $customer->save(); + + $this->assertEquals(5, $customer->primaryKey); + $this->assertEquals(5, $customer->oldPrimaryKey); + $this->assertEquals(5, $customer->$pkName); + $this->assertFalse($customer->isNewRecord); + } + + public function testUpdatePk() + { + $pkName = 'id'; + + $orderItem = Order::find([$pkName => 2]); + $this->assertEquals(2, $orderItem->primaryKey); + $this->assertEquals(2, $orderItem->oldPrimaryKey); + $this->assertEquals(2, $orderItem->$pkName); // $this->setExpectedException('yii\base\InvalidCallException'); - $orderItem->$pkName = 13; - $this->assertEquals(13, $orderItem->primaryKey); - $this->assertEquals(2, $orderItem->oldPrimaryKey); - $this->assertEquals(13, $orderItem->$pkName); - $orderItem->save(); - $this->afterSave(); - $this->assertEquals(13, $orderItem->primaryKey); - $this->assertEquals(13, $orderItem->oldPrimaryKey); - $this->assertEquals(13, $orderItem->$pkName); - - $this->assertNull(Order::find([$pkName => 2])); - $this->assertNotNull(Order::find([$pkName => 13])); - } - - public function testFindLazyVia2() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - /** @var Order $order */ - $orderClass = $this->getOrderClass(); - $pkName = 'id'; - - $order = new $orderClass(); - $order->$pkName = 100; - $this->assertEquals([], $order->items); - } - - /** - * Some PDO implementations(e.g. cubrid) do not support boolean values. - * Make sure this does not affect AR layer. - */ - public function testBooleanAttribute() - { - $db = $this->getConnection(); - Customer::deleteAll(); - Customer::setUpMapping($db->createCommand(), true); - - $customerClass = $this->getCustomerClass(); - $customer = new $customerClass(); - $customer->name = 'boolean customer'; - $customer->email = 'mail@example.com'; - $customer->status = true; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(true, $customer->status); - - $customer->status = false; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(false, $customer->status); - - $customer = new Customer(); - $customer->setAttributes(['email' => 'user2b@example.com', 'name' => 'user2b', 'status' => true], false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(['email' => 'user3b@example.com', 'name' => 'user3b', 'status' => false], false); - $customer->save(false); - $this->afterSave(); - - $customers = $this->callCustomerFind()->where(['status' => true])->all(); - $this->assertEquals(1, count($customers)); - - $customers = $this->callCustomerFind()->where(['status' => false])->all(); - $this->assertEquals(2, count($customers)); - } - - public function testFindAsArrayFields() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - // indexBy + asArray - $customers = $this->callCustomerFind()->asArray()->fields(['id', 'name'])->all(); - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers[0]); - $this->assertArrayHasKey('name', $customers[0]); - $this->assertArrayNotHasKey('email', $customers[0]); - $this->assertArrayNotHasKey('address', $customers[0]); - $this->assertArrayNotHasKey('status', $customers[0]); - $this->assertArrayHasKey('id', $customers[1]); - $this->assertArrayHasKey('name', $customers[1]); - $this->assertArrayNotHasKey('email', $customers[1]); - $this->assertArrayNotHasKey('address', $customers[1]); - $this->assertArrayNotHasKey('status', $customers[1]); - $this->assertArrayHasKey('id', $customers[2]); - $this->assertArrayHasKey('name', $customers[2]); - $this->assertArrayNotHasKey('email', $customers[2]); - $this->assertArrayNotHasKey('address', $customers[2]); - $this->assertArrayNotHasKey('status', $customers[2]); - } - - public function testFindIndexByFields() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // indexBy + asArray - $customers = $this->callCustomerFind()->indexBy('name')->fields('id', 'name')->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['user1'] instanceof $customerClass); - $this->assertTrue($customers['user2'] instanceof $customerClass); - $this->assertTrue($customers['user3'] instanceof $customerClass); - $this->assertNotNull($customers['user1']->id); - $this->assertNotNull($customers['user1']->name); - $this->assertNull($customers['user1']->email); - $this->assertNull($customers['user1']->address); - $this->assertNull($customers['user1']->status); - $this->assertNotNull($customers['user2']->id); - $this->assertNotNull($customers['user2']->name); - $this->assertNull($customers['user2']->email); - $this->assertNull($customers['user2']->address); - $this->assertNull($customers['user2']->status); - $this->assertNotNull($customers['user3']->id); - $this->assertNotNull($customers['user3']->name); - $this->assertNull($customers['user3']->email); - $this->assertNull($customers['user3']->address); - $this->assertNull($customers['user3']->status); - - // indexBy callable + asArray - $customers = $this->callCustomerFind()->indexBy(function ($customer) { - return $customer->id . '-' . $customer->name; - })->fields('id', 'name')->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['1-user1'] instanceof $customerClass); - $this->assertTrue($customers['2-user2'] instanceof $customerClass); - $this->assertTrue($customers['3-user3'] instanceof $customerClass); - $this->assertNotNull($customers['1-user1']->id); - $this->assertNotNull($customers['1-user1']->name); - $this->assertNull($customers['1-user1']->email); - $this->assertNull($customers['1-user1']->address); - $this->assertNull($customers['1-user1']->status); - $this->assertNotNull($customers['2-user2']->id); - $this->assertNotNull($customers['2-user2']->name); - $this->assertNull($customers['2-user2']->email); - $this->assertNull($customers['2-user2']->address); - $this->assertNull($customers['2-user2']->status); - $this->assertNotNull($customers['3-user3']->id); - $this->assertNotNull($customers['3-user3']->name); - $this->assertNull($customers['3-user3']->email); - $this->assertNull($customers['3-user3']->address); - $this->assertNull($customers['3-user3']->status); - } - - public function testFindIndexByAsArrayFields() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - // indexBy + asArray - $customers = $this->callCustomerFind()->indexBy('name')->asArray()->fields('id', 'name')->all(); - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers['user1']); - $this->assertArrayHasKey('name', $customers['user1']); - $this->assertArrayNotHasKey('email', $customers['user1']); - $this->assertArrayNotHasKey('address', $customers['user1']); - $this->assertArrayNotHasKey('status', $customers['user1']); - $this->assertArrayHasKey('id', $customers['user2']); - $this->assertArrayHasKey('name', $customers['user2']); - $this->assertArrayNotHasKey('email', $customers['user2']); - $this->assertArrayNotHasKey('address', $customers['user2']); - $this->assertArrayNotHasKey('status', $customers['user2']); - $this->assertArrayHasKey('id', $customers['user3']); - $this->assertArrayHasKey('name', $customers['user3']); - $this->assertArrayNotHasKey('email', $customers['user3']); - $this->assertArrayNotHasKey('address', $customers['user3']); - $this->assertArrayNotHasKey('status', $customers['user3']); - - // indexBy callable + asArray - $customers = $this->callCustomerFind()->indexBy(function ($customer) { - return $customer['id'] . '-' . $customer['name']; - })->asArray()->fields('id', 'name')->all(); - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers['1-user1']); - $this->assertArrayHasKey('name', $customers['1-user1']); - $this->assertArrayNotHasKey('email', $customers['1-user1']); - $this->assertArrayNotHasKey('address', $customers['1-user1']); - $this->assertArrayNotHasKey('status', $customers['1-user1']); - $this->assertArrayHasKey('id', $customers['2-user2']); - $this->assertArrayHasKey('name', $customers['2-user2']); - $this->assertArrayNotHasKey('email', $customers['2-user2']); - $this->assertArrayNotHasKey('address', $customers['2-user2']); - $this->assertArrayNotHasKey('status', $customers['2-user2']); - $this->assertArrayHasKey('id', $customers['3-user3']); - $this->assertArrayHasKey('name', $customers['3-user3']); - $this->assertArrayNotHasKey('email', $customers['3-user3']); - $this->assertArrayNotHasKey('address', $customers['3-user3']); - $this->assertArrayNotHasKey('status', $customers['3-user3']); - } - - public function testAfterFindGet() - { - /** @var BaseActiveRecord $customerClass */ - $customerClass = $this->getCustomerClass(); - - $afterFindCalls = []; - Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) { - /** @var BaseActiveRecord $ar */ - $ar = $event->sender; - $afterFindCalls[] = [get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; - }); - - $customer = Customer::get(1); - $this->assertNotNull($customer); - $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); - $afterFindCalls = []; - - $customer = Customer::mget([1, 2]); - $this->assertNotNull($customer); - $this->assertEquals([ - [$customerClass, false, 1, false], - [$customerClass, false, 2, false], - ], $afterFindCalls); - $afterFindCalls = []; - - Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND); - } - - // TODO test AR with not mapped PK + $orderItem->$pkName = 13; + $this->assertEquals(13, $orderItem->primaryKey); + $this->assertEquals(2, $orderItem->oldPrimaryKey); + $this->assertEquals(13, $orderItem->$pkName); + $orderItem->save(); + $this->afterSave(); + $this->assertEquals(13, $orderItem->primaryKey); + $this->assertEquals(13, $orderItem->oldPrimaryKey); + $this->assertEquals(13, $orderItem->$pkName); + + $this->assertNull(Order::find([$pkName => 2])); + $this->assertNotNull(Order::find([$pkName => 13])); + } + + public function testFindLazyVia2() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + /** @var Order $order */ + $orderClass = $this->getOrderClass(); + $pkName = 'id'; + + $order = new $orderClass(); + $order->$pkName = 100; + $this->assertEquals([], $order->items); + } + + /** + * Some PDO implementations(e.g. cubrid) do not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + $db = $this->getConnection(); + Customer::deleteAll(); + Customer::setUpMapping($db->createCommand(), true); + + $customerClass = $this->getCustomerClass(); + $customer = new $customerClass(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(true, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(false, $customer->status); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2b@example.com', 'name' => 'user2b', 'status' => true], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user3b@example.com', 'name' => 'user3b', 'status' => false], false); + $customer->save(false); + $this->afterSave(); + + $customers = $this->callCustomerFind()->where(['status' => true])->all(); + $this->assertEquals(1, count($customers)); + + $customers = $this->callCustomerFind()->where(['status' => false])->all(); + $this->assertEquals(2, count($customers)); + } + + public function testFindAsArrayFields() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + // indexBy + asArray + $customers = $this->callCustomerFind()->asArray()->fields(['id', 'name'])->all(); + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers[0]); + $this->assertArrayHasKey('name', $customers[0]); + $this->assertArrayNotHasKey('email', $customers[0]); + $this->assertArrayNotHasKey('address', $customers[0]); + $this->assertArrayNotHasKey('status', $customers[0]); + $this->assertArrayHasKey('id', $customers[1]); + $this->assertArrayHasKey('name', $customers[1]); + $this->assertArrayNotHasKey('email', $customers[1]); + $this->assertArrayNotHasKey('address', $customers[1]); + $this->assertArrayNotHasKey('status', $customers[1]); + $this->assertArrayHasKey('id', $customers[2]); + $this->assertArrayHasKey('name', $customers[2]); + $this->assertArrayNotHasKey('email', $customers[2]); + $this->assertArrayNotHasKey('address', $customers[2]); + $this->assertArrayNotHasKey('status', $customers[2]); + } + + public function testFindIndexByFields() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // indexBy + asArray + $customers = $this->callCustomerFind()->indexBy('name')->fields('id', 'name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['user1'] instanceof $customerClass); + $this->assertTrue($customers['user2'] instanceof $customerClass); + $this->assertTrue($customers['user3'] instanceof $customerClass); + $this->assertNotNull($customers['user1']->id); + $this->assertNotNull($customers['user1']->name); + $this->assertNull($customers['user1']->email); + $this->assertNull($customers['user1']->address); + $this->assertNull($customers['user1']->status); + $this->assertNotNull($customers['user2']->id); + $this->assertNotNull($customers['user2']->name); + $this->assertNull($customers['user2']->email); + $this->assertNull($customers['user2']->address); + $this->assertNull($customers['user2']->status); + $this->assertNotNull($customers['user3']->id); + $this->assertNotNull($customers['user3']->name); + $this->assertNull($customers['user3']->email); + $this->assertNull($customers['user3']->address); + $this->assertNull($customers['user3']->status); + + // indexBy callable + asArray + $customers = $this->callCustomerFind()->indexBy(function ($customer) { + return $customer->id . '-' . $customer->name; + })->fields('id', 'name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['1-user1'] instanceof $customerClass); + $this->assertTrue($customers['2-user2'] instanceof $customerClass); + $this->assertTrue($customers['3-user3'] instanceof $customerClass); + $this->assertNotNull($customers['1-user1']->id); + $this->assertNotNull($customers['1-user1']->name); + $this->assertNull($customers['1-user1']->email); + $this->assertNull($customers['1-user1']->address); + $this->assertNull($customers['1-user1']->status); + $this->assertNotNull($customers['2-user2']->id); + $this->assertNotNull($customers['2-user2']->name); + $this->assertNull($customers['2-user2']->email); + $this->assertNull($customers['2-user2']->address); + $this->assertNull($customers['2-user2']->status); + $this->assertNotNull($customers['3-user3']->id); + $this->assertNotNull($customers['3-user3']->name); + $this->assertNull($customers['3-user3']->email); + $this->assertNull($customers['3-user3']->address); + $this->assertNull($customers['3-user3']->status); + } + + public function testFindIndexByAsArrayFields() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + // indexBy + asArray + $customers = $this->callCustomerFind()->indexBy('name')->asArray()->fields('id', 'name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers['user1']); + $this->assertArrayHasKey('name', $customers['user1']); + $this->assertArrayNotHasKey('email', $customers['user1']); + $this->assertArrayNotHasKey('address', $customers['user1']); + $this->assertArrayNotHasKey('status', $customers['user1']); + $this->assertArrayHasKey('id', $customers['user2']); + $this->assertArrayHasKey('name', $customers['user2']); + $this->assertArrayNotHasKey('email', $customers['user2']); + $this->assertArrayNotHasKey('address', $customers['user2']); + $this->assertArrayNotHasKey('status', $customers['user2']); + $this->assertArrayHasKey('id', $customers['user3']); + $this->assertArrayHasKey('name', $customers['user3']); + $this->assertArrayNotHasKey('email', $customers['user3']); + $this->assertArrayNotHasKey('address', $customers['user3']); + $this->assertArrayNotHasKey('status', $customers['user3']); + + // indexBy callable + asArray + $customers = $this->callCustomerFind()->indexBy(function ($customer) { + return $customer['id'] . '-' . $customer['name']; + })->asArray()->fields('id', 'name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers['1-user1']); + $this->assertArrayHasKey('name', $customers['1-user1']); + $this->assertArrayNotHasKey('email', $customers['1-user1']); + $this->assertArrayNotHasKey('address', $customers['1-user1']); + $this->assertArrayNotHasKey('status', $customers['1-user1']); + $this->assertArrayHasKey('id', $customers['2-user2']); + $this->assertArrayHasKey('name', $customers['2-user2']); + $this->assertArrayNotHasKey('email', $customers['2-user2']); + $this->assertArrayNotHasKey('address', $customers['2-user2']); + $this->assertArrayNotHasKey('status', $customers['2-user2']); + $this->assertArrayHasKey('id', $customers['3-user3']); + $this->assertArrayHasKey('name', $customers['3-user3']); + $this->assertArrayNotHasKey('email', $customers['3-user3']); + $this->assertArrayNotHasKey('address', $customers['3-user3']); + $this->assertArrayNotHasKey('status', $customers['3-user3']); + } + + public function testAfterFindGet() + { + /** @var BaseActiveRecord $customerClass */ + $customerClass = $this->getCustomerClass(); + + $afterFindCalls = []; + Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) { + /** @var BaseActiveRecord $ar */ + $ar = $event->sender; + $afterFindCalls[] = [get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; + }); + + $customer = Customer::get(1); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = Customer::mget([1, 2]); + $this->assertNotNull($customer); + $this->assertEquals([ + [$customerClass, false, 1, false], + [$customerClass, false, 2, false], + ], $afterFindCalls); + $afterFindCalls = []; + + Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND); + } + + // TODO test AR with not mapped PK } diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php index 6d881895626..b0725857246 100644 --- a/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php +++ b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php @@ -9,19 +9,19 @@ */ class ElasticSearchConnectionTest extends ElasticSearchTestCase { - public function testOpen() - { - $connection = new Connection(); - $connection->autodetectCluster; - $connection->nodes = [ - ['http_address' => 'inet[/127.0.0.1:9200]'], - ]; - $this->assertNull($connection->activeNode); - $connection->open(); - $this->assertNotNull($connection->activeNode); - $this->assertArrayHasKey('name', reset($connection->nodes)); - $this->assertArrayHasKey('hostname', reset($connection->nodes)); - $this->assertArrayHasKey('version', reset($connection->nodes)); - $this->assertArrayHasKey('http_address', reset($connection->nodes)); - } + public function testOpen() + { + $connection = new Connection(); + $connection->autodetectCluster; + $connection->nodes = [ + ['http_address' => 'inet[/127.0.0.1:9200]'], + ]; + $this->assertNull($connection->activeNode); + $connection->open(); + $this->assertNotNull($connection->activeNode); + $this->assertArrayHasKey('name', reset($connection->nodes)); + $this->assertArrayHasKey('hostname', reset($connection->nodes)); + $this->assertArrayHasKey('version', reset($connection->nodes)); + $this->assertArrayHasKey('http_address', reset($connection->nodes)); + } } diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php b/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php index 2c280ed6e40..bdad9e6a584 100644 --- a/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php +++ b/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php @@ -13,39 +13,40 @@ */ class ElasticSearchTestCase extends TestCase { - protected function setUp() - { - $this->mockApplication(); - - $databases = $this->getParam('databases'); - $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : null; - if ($params === null || !isset($params['dsn'])) { - $this->markTestSkipped('No elasticsearch server connection configured.'); - } - $dsn = explode('/', $params['dsn']); - $host = $dsn[2]; - if (strpos($host, ':')===false) { - $host .= ':9200'; - } - if (!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) { - $this->markTestSkipped('No elasticsearch server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription); - } - - parent::setUp(); - } - - /** - * @param boolean $reset whether to clean up the test database - * @return Connection - */ - public function getConnection($reset = true) - { - $databases = $this->getParam('databases'); - $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : []; - $db = new Connection(); - if ($reset) { - $db->open(); - } - return $db; - } + protected function setUp() + { + $this->mockApplication(); + + $databases = $this->getParam('databases'); + $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : null; + if ($params === null || !isset($params['dsn'])) { + $this->markTestSkipped('No elasticsearch server connection configured.'); + } + $dsn = explode('/', $params['dsn']); + $host = $dsn[2]; + if (strpos($host, ':')===false) { + $host .= ':9200'; + } + if (!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No elasticsearch server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + parent::setUp(); + } + + /** + * @param boolean $reset whether to clean up the test database + * @return Connection + */ + public function getConnection($reset = true) + { + $databases = $this->getParam('databases'); + $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : []; + $db = new Connection(); + if ($reset) { + $db->open(); + } + + return $db; + } } diff --git a/tests/unit/extensions/elasticsearch/QueryBuilderTest.php b/tests/unit/extensions/elasticsearch/QueryBuilderTest.php index c7765a4bafb..82387617b62 100644 --- a/tests/unit/extensions/elasticsearch/QueryBuilderTest.php +++ b/tests/unit/extensions/elasticsearch/QueryBuilderTest.php @@ -11,66 +11,66 @@ class QueryBuilderTest extends ElasticSearchTestCase { - public function setUp() - { - parent::setUp(); - $command = $this->getConnection()->createCommand(); + public function setUp() + { + parent::setUp(); + $command = $this->getConnection()->createCommand(); - // delete index - if ($command->indexExists('yiitest')) { - $command->deleteIndex('yiitest'); - } - } + // delete index + if ($command->indexExists('yiitest')) { + $command->deleteIndex('yiitest'); + } + } - private function prepareDbData() - { - $command = $this->getConnection()->createCommand(); - $command->insert('yiitest', 'article', ['title' => 'I love yii!'], 1); - $command->insert('yiitest', 'article', ['title' => 'Symfony2 is another framework'], 2); - $command->insert('yiitest', 'article', ['title' => 'Yii2 out now!'], 3); - $command->insert('yiitest', 'article', ['title' => 'yii test'], 4); + private function prepareDbData() + { + $command = $this->getConnection()->createCommand(); + $command->insert('yiitest', 'article', ['title' => 'I love yii!'], 1); + $command->insert('yiitest', 'article', ['title' => 'Symfony2 is another framework'], 2); + $command->insert('yiitest', 'article', ['title' => 'Yii2 out now!'], 3); + $command->insert('yiitest', 'article', ['title' => 'yii test'], 4); - $command->flushIndex('yiitest'); - } + $command->flushIndex('yiitest'); + } - public function testQueryBuilderRespectsQuery() - { - $queryParts = ['field' => ['title' => 'yii']]; - $queryBuilder = new QueryBuilder($this->getConnection()); - $query = new Query(); - $query->query = $queryParts; - $build = $queryBuilder->build($query); - $this->assertTrue(array_key_exists('queryParts', $build)); - $this->assertTrue(array_key_exists('query', $build['queryParts'])); - $this->assertFalse(array_key_exists('match_all', $build['queryParts']), 'Match all should not be set'); - $this->assertSame($queryParts, $build['queryParts']['query']); - } + public function testQueryBuilderRespectsQuery() + { + $queryParts = ['field' => ['title' => 'yii']]; + $queryBuilder = new QueryBuilder($this->getConnection()); + $query = new Query(); + $query->query = $queryParts; + $build = $queryBuilder->build($query); + $this->assertTrue(array_key_exists('queryParts', $build)); + $this->assertTrue(array_key_exists('query', $build['queryParts'])); + $this->assertFalse(array_key_exists('match_all', $build['queryParts']), 'Match all should not be set'); + $this->assertSame($queryParts, $build['queryParts']['query']); + } - public function testYiiCanBeFoundByQuery() - { - $this->prepareDbData(); - $queryParts = ['field' => ['title' => 'yii']]; - $query = new Query(); - $query->from('yiitest', 'article'); - $query->query = $queryParts; - $result = $query->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - } + public function testYiiCanBeFoundByQuery() + { + $this->prepareDbData(); + $queryParts = ['field' => ['title' => 'yii']]; + $query = new Query(); + $query->from('yiitest', 'article'); + $query->query = $queryParts; + $result = $query->search($this->getConnection()); + $this->assertEquals(2, $result['hits']['total']); + } - public function testFuzzySearch() - { - $this->prepareDbData(); - $queryParts = [ - "fuzzy_like_this" => [ - "fields" => ["title"], - "like_text" => "Similar to YII", - "max_query_terms" => 4 - ] - ]; - $query = new Query(); - $query->from('yiitest', 'article'); - $query->query = $queryParts; - $result = $query->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - } + public function testFuzzySearch() + { + $this->prepareDbData(); + $queryParts = [ + "fuzzy_like_this" => [ + "fields" => ["title"], + "like_text" => "Similar to YII", + "max_query_terms" => 4 + ] + ]; + $query = new Query(); + $query->from('yiitest', 'article'); + $query->query = $queryParts; + $result = $query->search($this->getConnection()); + $this->assertEquals(3, $result['hits']['total']); + } } diff --git a/tests/unit/extensions/elasticsearch/QueryTest.php b/tests/unit/extensions/elasticsearch/QueryTest.php index 746f9a231ee..779dd2d58e9 100644 --- a/tests/unit/extensions/elasticsearch/QueryTest.php +++ b/tests/unit/extensions/elasticsearch/QueryTest.php @@ -9,180 +9,180 @@ */ class QueryTest extends ElasticSearchTestCase { - protected function setUp() - { - parent::setUp(); - - $command = $this->getConnection()->createCommand(); - - // delete index - if ($command->indexExists('yiitest')) { - $command->deleteIndex('yiitest'); - } - - $command->insert('yiitest', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); - $command->insert('yiitest', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); - $command->insert('yiitest', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); - $command->insert('yiitest', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); - - $command->flushIndex(); - } - - public function testFields() - { - $query = new Query; - $query->from('yiitest', 'user'); - - $query->fields(['name', 'status']); - $this->assertEquals(['name', 'status'], $query->fields); - - $query->fields('name', 'status'); - $this->assertEquals(['name', 'status'], $query->fields); - - $result = $query->one($this->getConnection()); - $this->assertEquals(2, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $query->fields([]); - $this->assertEquals([], $query->fields); - - $result = $query->one($this->getConnection()); - $this->assertEquals([], $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $query->fields(null); - $this->assertNull($query->fields); - - $result = $query->one($this->getConnection()); - $this->assertEquals(3, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - } - - public function testOne() - { - $query = new Query; - $query->from('yiitest', 'user'); - - $result = $query->one($this->getConnection()); - $this->assertEquals(3, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $result = $query->where(['name' => 'user1'])->one($this->getConnection()); - $this->assertEquals(3, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - $this->assertEquals(1, $result['_id']); - - $result = $query->where(['name' => 'user5'])->one($this->getConnection()); - $this->assertFalse($result); - } - - public function testAll() - { - $query = new Query; - $query->from('yiitest', 'user'); - - $results = $query->all($this->getConnection()); - $this->assertEquals(4, count($results)); - $result = reset($results); - $this->assertEquals(3, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $query = new Query; - $query->from('yiitest', 'user'); - - $results = $query->where(['name' => 'user1'])->all($this->getConnection()); - $this->assertEquals(1, count($results)); - $result = reset($results); - $this->assertEquals(3, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - $this->assertEquals(1, $result['_id']); - - // indexBy - $query = new Query; - $query->from('yiitest', 'user'); - - $results = $query->indexBy('name')->all($this->getConnection()); - $this->assertEquals(4, count($results)); - ksort($results); - $this->assertEquals(['user1', 'user2', 'user3', 'user4'], array_keys($results)); - } - - public function testScalar() - { - $query = new Query; - $query->from('yiitest', 'user'); - - $result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection()); - $this->assertEquals('user1', $result); - $result = $query->where(['name' => 'user1'])->scalar('noname', $this->getConnection()); - $this->assertNull($result); - $result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection()); - $this->assertNull($result); - } - - public function testColumn() - { - $query = new Query; - $query->from('yiitest', 'user'); - - $result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection()); - $this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result); - $result = $query->column('noname', $this->getConnection()); - $this->assertEquals([null, null, null, null], $result); - $result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection()); - $this->assertNull($result); - - } - - // TODO test facets - - // TODO test complex where() every edge of QueryBuilder - - public function testOrder() - { - $query = new Query; - $query->orderBy('team'); - $this->assertEquals(['team' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('company'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('age'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); - - $query->addOrderBy(['age' => SORT_DESC]); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); - - $query->addOrderBy('age ASC, company DESC'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); - } - - public function testLimitOffset() - { - $query = new Query; - $query->limit(10)->offset(5); - $this->assertEquals(10, $query->limit); - $this->assertEquals(5, $query->offset); - } - - public function testUnion() - { - } + protected function setUp() + { + parent::setUp(); + + $command = $this->getConnection()->createCommand(); + + // delete index + if ($command->indexExists('yiitest')) { + $command->deleteIndex('yiitest'); + } + + $command->insert('yiitest', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); + $command->insert('yiitest', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); + $command->insert('yiitest', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); + $command->insert('yiitest', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); + + $command->flushIndex(); + } + + public function testFields() + { + $query = new Query; + $query->from('yiitest', 'user'); + + $query->fields(['name', 'status']); + $this->assertEquals(['name', 'status'], $query->fields); + + $query->fields('name', 'status'); + $this->assertEquals(['name', 'status'], $query->fields); + + $result = $query->one($this->getConnection()); + $this->assertEquals(2, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + + $query->fields([]); + $this->assertEquals([], $query->fields); + + $result = $query->one($this->getConnection()); + $this->assertEquals([], $result['_source']); + $this->assertArrayHasKey('_id', $result); + + $query->fields(null); + $this->assertNull($query->fields); + + $result = $query->one($this->getConnection()); + $this->assertEquals(3, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('email', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + } + + public function testOne() + { + $query = new Query; + $query->from('yiitest', 'user'); + + $result = $query->one($this->getConnection()); + $this->assertEquals(3, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('email', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + + $result = $query->where(['name' => 'user1'])->one($this->getConnection()); + $this->assertEquals(3, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('email', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + $this->assertEquals(1, $result['_id']); + + $result = $query->where(['name' => 'user5'])->one($this->getConnection()); + $this->assertFalse($result); + } + + public function testAll() + { + $query = new Query; + $query->from('yiitest', 'user'); + + $results = $query->all($this->getConnection()); + $this->assertEquals(4, count($results)); + $result = reset($results); + $this->assertEquals(3, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('email', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + + $query = new Query; + $query->from('yiitest', 'user'); + + $results = $query->where(['name' => 'user1'])->all($this->getConnection()); + $this->assertEquals(1, count($results)); + $result = reset($results); + $this->assertEquals(3, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('email', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + $this->assertEquals(1, $result['_id']); + + // indexBy + $query = new Query; + $query->from('yiitest', 'user'); + + $results = $query->indexBy('name')->all($this->getConnection()); + $this->assertEquals(4, count($results)); + ksort($results); + $this->assertEquals(['user1', 'user2', 'user3', 'user4'], array_keys($results)); + } + + public function testScalar() + { + $query = new Query; + $query->from('yiitest', 'user'); + + $result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection()); + $this->assertEquals('user1', $result); + $result = $query->where(['name' => 'user1'])->scalar('noname', $this->getConnection()); + $this->assertNull($result); + $result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection()); + $this->assertNull($result); + } + + public function testColumn() + { + $query = new Query; + $query->from('yiitest', 'user'); + + $result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection()); + $this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result); + $result = $query->column('noname', $this->getConnection()); + $this->assertEquals([null, null, null, null], $result); + $result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection()); + $this->assertNull($result); + + } + + // TODO test facets + + // TODO test complex where() every edge of QueryBuilder + + public function testOrder() + { + $query = new Query; + $query->orderBy('team'); + $this->assertEquals(['team' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('company'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('age'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); + + $query->addOrderBy(['age' => SORT_DESC]); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); + + $query->addOrderBy('age ASC, company DESC'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); + } + + public function testLimitOffset() + { + $query = new Query; + $query->limit(10)->offset(5); + $this->assertEquals(10, $query->limit); + $this->assertEquals(5, $query->offset); + } + + public function testUnion() + { + } } diff --git a/tests/unit/extensions/imagine/AbstractImageTest.php b/tests/unit/extensions/imagine/AbstractImageTest.php index cc076c1c56f..aac94d58fc2 100644 --- a/tests/unit/extensions/imagine/AbstractImageTest.php +++ b/tests/unit/extensions/imagine/AbstractImageTest.php @@ -10,89 +10,89 @@ abstract class AbstractImageTest extends VendorTestCase { - protected $imageFile; - protected $watermarkFile; - protected $runtimeTextFile; - protected $runtimeWatermarkFile; - - protected function setUp() - { - $this->imageFile = Yii::getAlias('@yiiunit/data/imagine/large') . '.jpg'; - $this->watermarkFile = Yii::getAlias('@yiiunit/data/imagine/xparent') . '.gif'; - $this->runtimeTextFile = Yii::getAlias('@yiiunit/runtime/image-text-test') . '.png'; - $this->runtimeWatermarkFile = Yii::getAlias('@yiiunit/runtime/image-watermark-test') . '.png'; - parent::setUp(); - } - - protected function tearDown() - { - @unlink($this->runtimeTextFile); - @unlink($this->runtimeWatermarkFile); - } - - public function testText() - { - if (!$this->isFontTestSupported()) { - $this->markTestSkipped('Skipping ImageGdTest Gd not installed'); - } - - $fontFile = Yii::getAlias('@yiiunit/data/imagine/GothamRnd-Light') . '.otf'; - - $img = Image::text($this->imageFile, 'Yii-2 Image', $fontFile, [0, 0], [ - 'size' => 12, - 'color' => '000' - ]); - - $img->save($this->runtimeTextFile); - $this->assertTrue(file_exists($this->runtimeTextFile)); - - } - - public function testCrop() - { - $point = [20, 20]; - $img = Image::crop($this->imageFile, 100, 100, $point); - - $this->assertEquals(100, $img->getSize()->getWidth()); - $this->assertEquals(100, $img->getSize()->getHeight()); - - } - - public function testWatermark() - { - $img = Image::watermark($this->imageFile, $this->watermarkFile); - $img->save($this->runtimeWatermarkFile); - $this->assertTrue(file_exists($this->runtimeWatermarkFile)); - } - - public function testFrame() - { - $frameSize = 5; - $original = Image::getImagine()->open($this->imageFile); - $originalSize = $original->getSize(); - $img = Image::frame($this->imageFile, $frameSize, '666', 0); - $size = $img->getSize(); - - $this->assertEquals($size->getWidth(), $originalSize->getWidth() + ($frameSize * 2)); - } - - public function testThumbnail() - { - $img = Image::thumbnail($this->imageFile, 120, 120); - - $this->assertEquals(120, $img->getSize()->getWidth()); - $this->assertEquals(120, $img->getSize()->getHeight()); - } - - /** - * @expectedException \yii\base\InvalidConfigException - */ - public function testShouldThrowExceptionOnDriverInvalidArgument() - { - Image::setImagine(null); - Image::$driver = 'fake-driver'; - Image::getImagine(); - } - - abstract protected function isFontTestSupported(); + protected $imageFile; + protected $watermarkFile; + protected $runtimeTextFile; + protected $runtimeWatermarkFile; + + protected function setUp() + { + $this->imageFile = Yii::getAlias('@yiiunit/data/imagine/large') . '.jpg'; + $this->watermarkFile = Yii::getAlias('@yiiunit/data/imagine/xparent') . '.gif'; + $this->runtimeTextFile = Yii::getAlias('@yiiunit/runtime/image-text-test') . '.png'; + $this->runtimeWatermarkFile = Yii::getAlias('@yiiunit/runtime/image-watermark-test') . '.png'; + parent::setUp(); + } + + protected function tearDown() + { + @unlink($this->runtimeTextFile); + @unlink($this->runtimeWatermarkFile); + } + + public function testText() + { + if (!$this->isFontTestSupported()) { + $this->markTestSkipped('Skipping ImageGdTest Gd not installed'); + } + + $fontFile = Yii::getAlias('@yiiunit/data/imagine/GothamRnd-Light') . '.otf'; + + $img = Image::text($this->imageFile, 'Yii-2 Image', $fontFile, [0, 0], [ + 'size' => 12, + 'color' => '000' + ]); + + $img->save($this->runtimeTextFile); + $this->assertTrue(file_exists($this->runtimeTextFile)); + + } + + public function testCrop() + { + $point = [20, 20]; + $img = Image::crop($this->imageFile, 100, 100, $point); + + $this->assertEquals(100, $img->getSize()->getWidth()); + $this->assertEquals(100, $img->getSize()->getHeight()); + + } + + public function testWatermark() + { + $img = Image::watermark($this->imageFile, $this->watermarkFile); + $img->save($this->runtimeWatermarkFile); + $this->assertTrue(file_exists($this->runtimeWatermarkFile)); + } + + public function testFrame() + { + $frameSize = 5; + $original = Image::getImagine()->open($this->imageFile); + $originalSize = $original->getSize(); + $img = Image::frame($this->imageFile, $frameSize, '666', 0); + $size = $img->getSize(); + + $this->assertEquals($size->getWidth(), $originalSize->getWidth() + ($frameSize * 2)); + } + + public function testThumbnail() + { + $img = Image::thumbnail($this->imageFile, 120, 120); + + $this->assertEquals(120, $img->getSize()->getWidth()); + $this->assertEquals(120, $img->getSize()->getHeight()); + } + + /** + * @expectedException \yii\base\InvalidConfigException + */ + public function testShouldThrowExceptionOnDriverInvalidArgument() + { + Image::setImagine(null); + Image::$driver = 'fake-driver'; + Image::getImagine(); + } + + abstract protected function isFontTestSupported(); } diff --git a/tests/unit/extensions/imagine/ImageGdTest.php b/tests/unit/extensions/imagine/ImageGdTest.php index c3597c57623..322ceb17a15 100644 --- a/tests/unit/extensions/imagine/ImageGdTest.php +++ b/tests/unit/extensions/imagine/ImageGdTest.php @@ -10,20 +10,21 @@ */ class ImageGdTest extends AbstractImageTest { - protected function setUp() - { - if (!function_exists('gd_info')) { - $this->markTestSkipped('Skipping ImageGdTest, Gd not installed'); - } else { - Image::setImagine(null); - Image::$driver = Image::DRIVER_GD2; - parent::setUp(); - } - } + protected function setUp() + { + if (!function_exists('gd_info')) { + $this->markTestSkipped('Skipping ImageGdTest, Gd not installed'); + } else { + Image::setImagine(null); + Image::$driver = Image::DRIVER_GD2; + parent::setUp(); + } + } - protected function isFontTestSupported() - { - $infos = gd_info(); - return isset($infos['FreeType Support']) ? $infos['FreeType Support'] : false; - } + protected function isFontTestSupported() + { + $infos = gd_info(); + + return isset($infos['FreeType Support']) ? $infos['FreeType Support'] : false; + } } diff --git a/tests/unit/extensions/imagine/ImageGmagickTest.php b/tests/unit/extensions/imagine/ImageGmagickTest.php index abad5092ca9..bfa4e943237 100644 --- a/tests/unit/extensions/imagine/ImageGmagickTest.php +++ b/tests/unit/extensions/imagine/ImageGmagickTest.php @@ -11,19 +11,19 @@ class ImageGmagickTest extends AbstractImageTest { - protected function setUp() - { - if (!class_exists('Gmagick')) { - $this->markTestSkipped('Skipping ImageGmagickTest, Gmagick is not installed'); - } else { - Image::setImagine(null); - Image::$driver = Image::DRIVER_GMAGICK; - parent::setUp(); - } - } + protected function setUp() + { + if (!class_exists('Gmagick')) { + $this->markTestSkipped('Skipping ImageGmagickTest, Gmagick is not installed'); + } else { + Image::setImagine(null); + Image::$driver = Image::DRIVER_GMAGICK; + parent::setUp(); + } + } - protected function isFontTestSupported() - { - return true; - } + protected function isFontTestSupported() + { + return true; + } } diff --git a/tests/unit/extensions/imagine/ImageImagickTest.php b/tests/unit/extensions/imagine/ImageImagickTest.php index f971f64106a..27bc0a14f63 100644 --- a/tests/unit/extensions/imagine/ImageImagickTest.php +++ b/tests/unit/extensions/imagine/ImageImagickTest.php @@ -11,19 +11,19 @@ class ImageImagickTest extends AbstractImageTest { - protected function setUp() - { - if (!class_exists('Imagick')) { - $this->markTestSkipped('Skipping ImageImagickTest, Imagick is not installed'); - } else { - Image::setImagine(null); - Image::$driver = Image::DRIVER_IMAGICK; - parent::setUp(); - } - } + protected function setUp() + { + if (!class_exists('Imagick')) { + $this->markTestSkipped('Skipping ImageImagickTest, Imagick is not installed'); + } else { + Image::setImagine(null); + Image::$driver = Image::DRIVER_IMAGICK; + parent::setUp(); + } + } - protected function isFontTestSupported() - { - return true; - } + protected function isFontTestSupported() + { + return true; + } } diff --git a/tests/unit/extensions/mongodb/ActiveDataProviderTest.php b/tests/unit/extensions/mongodb/ActiveDataProviderTest.php index 5cf67e1da79..f90c5d118b2 100644 --- a/tests/unit/extensions/mongodb/ActiveDataProviderTest.php +++ b/tests/unit/extensions/mongodb/ActiveDataProviderTest.php @@ -12,80 +12,80 @@ */ class ActiveDataProviderTest extends MongoDbTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - $this->setUpTestRows(); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + $this->setUpTestRows(); + } - protected function tearDown() - { - $this->dropCollection(Customer::collectionName()); - parent::tearDown(); - } + protected function tearDown() + { + $this->dropCollection(Customer::collectionName()); + parent::tearDown(); + } - /** - * Sets up test rows. - */ - protected function setUpTestRows() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = []; - for ($i = 1; $i <= 10; $i++) { - $rows[] = [ - 'name' => 'name' . $i, - 'email' => 'email' . $i, - 'address' => 'address' . $i, - 'status' => $i, - ]; - } - $collection->batchInsert($rows); - } + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = []; + for ($i = 1; $i <= 10; $i++) { + $rows[] = [ + 'name' => 'name' . $i, + 'email' => 'email' . $i, + 'address' => 'address' . $i, + 'status' => $i, + ]; + } + $collection->batchInsert($rows); + } - // Tests : + // Tests : - public function testQuery() - { - $query = new Query; - $query->from('customer'); + public function testQuery() + { + $query = new Query; + $query->from('customer'); - $provider = new ActiveDataProvider([ - 'query' => $query, - 'db' => $this->getConnection(), - ]); - $models = $provider->getModels(); - $this->assertEquals(10, count($models)); + $provider = new ActiveDataProvider([ + 'query' => $query, + 'db' => $this->getConnection(), + ]); + $models = $provider->getModels(); + $this->assertEquals(10, count($models)); - $provider = new ActiveDataProvider([ - 'query' => $query, - 'db' => $this->getConnection(), - 'pagination' => [ - 'pageSize' => 5, - ] - ]); - $models = $provider->getModels(); - $this->assertEquals(5, count($models)); - } + $provider = new ActiveDataProvider([ + 'query' => $query, + 'db' => $this->getConnection(), + 'pagination' => [ + 'pageSize' => 5, + ] + ]); + $models = $provider->getModels(); + $this->assertEquals(5, count($models)); + } - public function testActiveQuery() - { - $provider = new ActiveDataProvider([ - 'query' => Customer::find()->orderBy('id ASC'), - ]); - $models = $provider->getModels(); - $this->assertEquals(10, count($models)); - $this->assertTrue($models[0] instanceof Customer); - $keys = $provider->getKeys(); - $this->assertTrue($keys[0] instanceof \MongoId); + public function testActiveQuery() + { + $provider = new ActiveDataProvider([ + 'query' => Customer::find()->orderBy('id ASC'), + ]); + $models = $provider->getModels(); + $this->assertEquals(10, count($models)); + $this->assertTrue($models[0] instanceof Customer); + $keys = $provider->getKeys(); + $this->assertTrue($keys[0] instanceof \MongoId); - $provider = new ActiveDataProvider([ - 'query' => Customer::find(), - 'pagination' => [ - 'pageSize' => 5, - ] - ]); - $models = $provider->getModels(); - $this->assertEquals(5, count($models)); - } + $provider = new ActiveDataProvider([ + 'query' => Customer::find(), + 'pagination' => [ + 'pageSize' => 5, + ] + ]); + $models = $provider->getModels(); + $this->assertEquals(5, count($models)); + } } diff --git a/tests/unit/extensions/mongodb/ActiveRecordTest.php b/tests/unit/extensions/mongodb/ActiveRecordTest.php index f334d9b0c96..469d9b411b2 100644 --- a/tests/unit/extensions/mongodb/ActiveRecordTest.php +++ b/tests/unit/extensions/mongodb/ActiveRecordTest.php @@ -11,256 +11,256 @@ */ class ActiveRecordTest extends MongoDbTestCase { - /** - * @var array[] list of test rows. - */ - protected $testRows = []; - - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - $this->setUpTestRows(); - } - - protected function tearDown() - { - $this->dropCollection(Customer::collectionName()); - parent::tearDown(); - } - - /** - * Sets up test rows. - */ - protected function setUpTestRows() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = []; - for ($i = 1; $i <= 10; $i++) { - $rows[] = [ - 'name' => 'name' . $i, - 'email' => 'email' . $i, - 'address' => 'address' . $i, - 'status' => $i, - ]; - } - $collection->batchInsert($rows); - $this->testRows = $rows; - } - - // Tests : - - public function testFind() - { - // find one - $result = Customer::find(); - $this->assertTrue($result instanceof ActiveQuery); - $customer = $result->one(); - $this->assertTrue($customer instanceof Customer); - - // find all - $customers = Customer::find()->all(); - $this->assertEquals(10, count($customers)); - $this->assertTrue($customers[0] instanceof Customer); - $this->assertTrue($customers[1] instanceof Customer); - - // find by _id - $testId = $this->testRows[0]['_id']; - $customer = Customer::find($testId); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals($testId, $customer->_id); - - // find by column values - $customer = Customer::find(['name' => 'name5']); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals($this->testRows[4]['_id'], $customer->_id); - $this->assertEquals('name5', $customer->name); - $customer = Customer::find(['name' => 'unexisting name']); - $this->assertNull($customer); - - // find by attributes - $customer = Customer::find()->where(['status' => 4])->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(4, $customer->status); - - // find count, sum, average, min, max, distinct - $this->assertEquals(10, Customer::find()->count()); - $this->assertEquals(1, Customer::find()->where(['status' => 2])->count()); - $this->assertEquals((1+10)/2*10, Customer::find()->sum('status')); - $this->assertEquals((1+10)/2, Customer::find()->average('status')); - $this->assertEquals(1, Customer::find()->min('status')); - $this->assertEquals(10, Customer::find()->max('status')); - $this->assertEquals(range(1, 10), Customer::find()->distinct('status')); - - // scope - $this->assertEquals(1, Customer::find()->activeOnly()->count()); - - // asArray - $testRow = $this->testRows[2]; - $customer = Customer::find()->where(['_id' => $testRow['_id']])->asArray()->one(); - $this->assertEquals($testRow, $customer); - - // indexBy - $customers = Customer::find()->indexBy('name')->all(); - $this->assertTrue($customers['name1'] instanceof Customer); - $this->assertTrue($customers['name2'] instanceof Customer); - - // indexBy callable - $customers = Customer::find()->indexBy(function ($customer) { - return $customer->status . '-' . $customer->status; - })->all(); - $this->assertTrue($customers['1-1'] instanceof Customer); - $this->assertTrue($customers['2-2'] instanceof Customer); - } - - public function testInsert() - { - $record = new Customer; - $record->name = 'new name'; - $record->email = 'new email'; - $record->address = 'new address'; - $record->status = 7; - - $this->assertTrue($record->isNewRecord); - - $record->save(); - - $this->assertTrue($record->_id instanceof \MongoId); - $this->assertFalse($record->isNewRecord); - } - - /** - * @depends testInsert - */ - public function testUpdate() - { - $record = new Customer; - $record->name = 'new name'; - $record->email = 'new email'; - $record->address = 'new address'; - $record->status = 7; - $record->save(); - - // save - $record = Customer::find($record->_id); - $this->assertTrue($record instanceof Customer); - $this->assertEquals(7, $record->status); - $this->assertFalse($record->isNewRecord); - - $record->status = 9; - $record->save(); - $this->assertEquals(9, $record->status); - $this->assertFalse($record->isNewRecord); - $record2 = Customer::find($record->_id); - $this->assertEquals(9, $record2->status); - - // updateAll - $pk = ['_id' => $record->_id]; - $ret = Customer::updateAll(['status' => 55], $pk); - $this->assertEquals(1, $ret); - $record = Customer::find($pk); - $this->assertEquals(55, $record->status); - } - - /** - * @depends testInsert - */ - public function testDelete() - { - // delete - $record = new Customer; - $record->name = 'new name'; - $record->email = 'new email'; - $record->address = 'new address'; - $record->status = 7; - $record->save(); - - $record = Customer::find($record->_id); - $record->delete(); - $record = Customer::find($record->_id); - $this->assertNull($record); - - // deleteAll - $record = new Customer; - $record->name = 'new name'; - $record->email = 'new email'; - $record->address = 'new address'; - $record->status = 7; - $record->save(); - - $ret = Customer::deleteAll(['name' => 'new name']); - $this->assertEquals(1, $ret); - $records = Customer::find()->where(['name' => 'new name'])->all(); - $this->assertEquals(0, count($records)); - } - - public function testUpdateAllCounters() - { - $this->assertEquals(1, Customer::updateAllCounters(['status' => 10], ['status' => 10])); - - $record = Customer::find(['status' => 10]); - $this->assertNull($record); - } - - /** - * @depends testUpdateAllCounters - */ - public function testUpdateCounters() - { - $record = Customer::find($this->testRows[9]); - - $originalCounter = $record->status; - $counterIncrement = 20; - $record->updateCounters(['status' => $counterIncrement]); - $this->assertEquals($originalCounter + $counterIncrement, $record->status); - - $refreshedRecord = Customer::find($record->_id); - $this->assertEquals($originalCounter + $counterIncrement, $refreshedRecord->status); - } - - /** - * @depends testUpdate - */ - public function testUpdateNestedAttribute() - { - $record = new Customer; - $record->name = 'new name'; - $record->email = 'new email'; - $record->address = [ - 'city' => 'SomeCity', - 'street' => 'SomeStreet', - ]; - $record->status = 7; - $record->save(); - - // save - $record = Customer::find($record->_id); - $newAddress = [ - 'city' => 'AnotherCity' - ]; - $record->address = $newAddress; - $record->save(); - $record2 = Customer::find($record->_id); - $this->assertEquals($newAddress, $record2->address); - } - - /** - * @depends testFind - * @depends testInsert - */ - public function testQueryByIntegerField() - { - $record = new Customer; - $record->name = 'new name'; - $record->status = 7; - $record->save(); - - $row = Customer::find()->where(['status' => 7])->one(); - $this->assertNotEmpty($row); - $this->assertEquals(7, $row->status); - - $rowRefreshed = Customer::find()->where(['status' => $row->status])->one(); - $this->assertNotEmpty($rowRefreshed); - $this->assertEquals(7, $rowRefreshed->status); - } + /** + * @var array[] list of test rows. + */ + protected $testRows = []; + + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + $this->setUpTestRows(); + } + + protected function tearDown() + { + $this->dropCollection(Customer::collectionName()); + parent::tearDown(); + } + + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = []; + for ($i = 1; $i <= 10; $i++) { + $rows[] = [ + 'name' => 'name' . $i, + 'email' => 'email' . $i, + 'address' => 'address' . $i, + 'status' => $i, + ]; + } + $collection->batchInsert($rows); + $this->testRows = $rows; + } + + // Tests : + + public function testFind() + { + // find one + $result = Customer::find(); + $this->assertTrue($result instanceof ActiveQuery); + $customer = $result->one(); + $this->assertTrue($customer instanceof Customer); + + // find all + $customers = Customer::find()->all(); + $this->assertEquals(10, count($customers)); + $this->assertTrue($customers[0] instanceof Customer); + $this->assertTrue($customers[1] instanceof Customer); + + // find by _id + $testId = $this->testRows[0]['_id']; + $customer = Customer::find($testId); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals($testId, $customer->_id); + + // find by column values + $customer = Customer::find(['name' => 'name5']); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals($this->testRows[4]['_id'], $customer->_id); + $this->assertEquals('name5', $customer->name); + $customer = Customer::find(['name' => 'unexisting name']); + $this->assertNull($customer); + + // find by attributes + $customer = Customer::find()->where(['status' => 4])->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals(4, $customer->status); + + // find count, sum, average, min, max, distinct + $this->assertEquals(10, Customer::find()->count()); + $this->assertEquals(1, Customer::find()->where(['status' => 2])->count()); + $this->assertEquals((1+10)/2*10, Customer::find()->sum('status')); + $this->assertEquals((1+10)/2, Customer::find()->average('status')); + $this->assertEquals(1, Customer::find()->min('status')); + $this->assertEquals(10, Customer::find()->max('status')); + $this->assertEquals(range(1, 10), Customer::find()->distinct('status')); + + // scope + $this->assertEquals(1, Customer::find()->activeOnly()->count()); + + // asArray + $testRow = $this->testRows[2]; + $customer = Customer::find()->where(['_id' => $testRow['_id']])->asArray()->one(); + $this->assertEquals($testRow, $customer); + + // indexBy + $customers = Customer::find()->indexBy('name')->all(); + $this->assertTrue($customers['name1'] instanceof Customer); + $this->assertTrue($customers['name2'] instanceof Customer); + + // indexBy callable + $customers = Customer::find()->indexBy(function ($customer) { + return $customer->status . '-' . $customer->status; + })->all(); + $this->assertTrue($customers['1-1'] instanceof Customer); + $this->assertTrue($customers['2-2'] instanceof Customer); + } + + public function testInsert() + { + $record = new Customer; + $record->name = 'new name'; + $record->email = 'new email'; + $record->address = 'new address'; + $record->status = 7; + + $this->assertTrue($record->isNewRecord); + + $record->save(); + + $this->assertTrue($record->_id instanceof \MongoId); + $this->assertFalse($record->isNewRecord); + } + + /** + * @depends testInsert + */ + public function testUpdate() + { + $record = new Customer; + $record->name = 'new name'; + $record->email = 'new email'; + $record->address = 'new address'; + $record->status = 7; + $record->save(); + + // save + $record = Customer::find($record->_id); + $this->assertTrue($record instanceof Customer); + $this->assertEquals(7, $record->status); + $this->assertFalse($record->isNewRecord); + + $record->status = 9; + $record->save(); + $this->assertEquals(9, $record->status); + $this->assertFalse($record->isNewRecord); + $record2 = Customer::find($record->_id); + $this->assertEquals(9, $record2->status); + + // updateAll + $pk = ['_id' => $record->_id]; + $ret = Customer::updateAll(['status' => 55], $pk); + $this->assertEquals(1, $ret); + $record = Customer::find($pk); + $this->assertEquals(55, $record->status); + } + + /** + * @depends testInsert + */ + public function testDelete() + { + // delete + $record = new Customer; + $record->name = 'new name'; + $record->email = 'new email'; + $record->address = 'new address'; + $record->status = 7; + $record->save(); + + $record = Customer::find($record->_id); + $record->delete(); + $record = Customer::find($record->_id); + $this->assertNull($record); + + // deleteAll + $record = new Customer; + $record->name = 'new name'; + $record->email = 'new email'; + $record->address = 'new address'; + $record->status = 7; + $record->save(); + + $ret = Customer::deleteAll(['name' => 'new name']); + $this->assertEquals(1, $ret); + $records = Customer::find()->where(['name' => 'new name'])->all(); + $this->assertEquals(0, count($records)); + } + + public function testUpdateAllCounters() + { + $this->assertEquals(1, Customer::updateAllCounters(['status' => 10], ['status' => 10])); + + $record = Customer::find(['status' => 10]); + $this->assertNull($record); + } + + /** + * @depends testUpdateAllCounters + */ + public function testUpdateCounters() + { + $record = Customer::find($this->testRows[9]); + + $originalCounter = $record->status; + $counterIncrement = 20; + $record->updateCounters(['status' => $counterIncrement]); + $this->assertEquals($originalCounter + $counterIncrement, $record->status); + + $refreshedRecord = Customer::find($record->_id); + $this->assertEquals($originalCounter + $counterIncrement, $refreshedRecord->status); + } + + /** + * @depends testUpdate + */ + public function testUpdateNestedAttribute() + { + $record = new Customer; + $record->name = 'new name'; + $record->email = 'new email'; + $record->address = [ + 'city' => 'SomeCity', + 'street' => 'SomeStreet', + ]; + $record->status = 7; + $record->save(); + + // save + $record = Customer::find($record->_id); + $newAddress = [ + 'city' => 'AnotherCity' + ]; + $record->address = $newAddress; + $record->save(); + $record2 = Customer::find($record->_id); + $this->assertEquals($newAddress, $record2->address); + } + + /** + * @depends testFind + * @depends testInsert + */ + public function testQueryByIntegerField() + { + $record = new Customer; + $record->name = 'new name'; + $record->status = 7; + $record->save(); + + $row = Customer::find()->where(['status' => 7])->one(); + $this->assertNotEmpty($row); + $this->assertEquals(7, $row->status); + + $rowRefreshed = Customer::find()->where(['status' => $row->status])->one(); + $this->assertNotEmpty($rowRefreshed); + $this->assertEquals(7, $rowRefreshed->status); + } } diff --git a/tests/unit/extensions/mongodb/ActiveRelationTest.php b/tests/unit/extensions/mongodb/ActiveRelationTest.php index 2baeab43f9d..7025f6c6768 100644 --- a/tests/unit/extensions/mongodb/ActiveRelationTest.php +++ b/tests/unit/extensions/mongodb/ActiveRelationTest.php @@ -11,76 +11,76 @@ */ class ActiveRelationTest extends MongoDbTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - $this->setUpTestRows(); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + $this->setUpTestRows(); + } - protected function tearDown() - { - $this->dropCollection(Customer::collectionName()); - $this->dropCollection(CustomerOrder::collectionName()); - parent::tearDown(); - } + protected function tearDown() + { + $this->dropCollection(Customer::collectionName()); + $this->dropCollection(CustomerOrder::collectionName()); + parent::tearDown(); + } - /** - * Sets up test rows. - */ - protected function setUpTestRows() - { - $customerCollection = $this->getConnection()->getCollection('customer'); + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $customerCollection = $this->getConnection()->getCollection('customer'); - $customers = []; - for ($i = 1; $i <= 5; $i++) { - $customers[] = [ - 'name' => 'name' . $i, - 'email' => 'email' . $i, - 'address' => 'address' . $i, - 'status' => $i, - ]; - } - $customerCollection->batchInsert($customers); + $customers = []; + for ($i = 1; $i <= 5; $i++) { + $customers[] = [ + 'name' => 'name' . $i, + 'email' => 'email' . $i, + 'address' => 'address' . $i, + 'status' => $i, + ]; + } + $customerCollection->batchInsert($customers); - $customerOrderCollection = $this->getConnection()->getCollection('customer_order'); - $customerOrders = []; - foreach ($customers as $customer) { - $customerOrders[] = [ - 'customer_id' => $customer['_id'], - 'number' => $customer['status'], - ]; - $customerOrders[] = [ - 'customer_id' => $customer['_id'], - 'number' => $customer['status'] + 100, - ]; - } - $customerOrderCollection->batchInsert($customerOrders); - } + $customerOrderCollection = $this->getConnection()->getCollection('customer_order'); + $customerOrders = []; + foreach ($customers as $customer) { + $customerOrders[] = [ + 'customer_id' => $customer['_id'], + 'number' => $customer['status'], + ]; + $customerOrders[] = [ + 'customer_id' => $customer['_id'], + 'number' => $customer['status'] + 100, + ]; + } + $customerOrderCollection->batchInsert($customerOrders); + } - // Tests : + // Tests : - public function testFindLazy() - { - /** @var CustomerOrder $order */ - $order = CustomerOrder::find(['number' => 2]); - $this->assertFalse($order->isRelationPopulated('customer')); - $customer = $order->customer; - $this->assertTrue($order->isRelationPopulated('customer')); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals((string)$customer->_id, (string)$order->customer_id); - $this->assertEquals(1, count($order->relatedRecords)); - } + public function testFindLazy() + { + /** @var CustomerOrder $order */ + $order = CustomerOrder::find(['number' => 2]); + $this->assertFalse($order->isRelationPopulated('customer')); + $customer = $order->customer; + $this->assertTrue($order->isRelationPopulated('customer')); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals((string) $customer->_id, (string) $order->customer_id); + $this->assertEquals(1, count($order->relatedRecords)); + } - public function testFindEager() - { - $orders = CustomerOrder::find()->with('customer')->all(); - $this->assertEquals(10, count($orders)); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - $this->assertTrue($orders[1]->isRelationPopulated('customer')); - $this->assertTrue($orders[0]->customer instanceof Customer); - $this->assertEquals((string)$orders[0]->customer->_id, (string)$orders[0]->customer_id); - $this->assertTrue($orders[1]->customer instanceof Customer); - $this->assertEquals((string)$orders[1]->customer->_id, (string)$orders[1]->customer_id); - } + public function testFindEager() + { + $orders = CustomerOrder::find()->with('customer')->all(); + $this->assertEquals(10, count($orders)); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[0]->customer instanceof Customer); + $this->assertEquals((string) $orders[0]->customer->_id, (string) $orders[0]->customer_id); + $this->assertTrue($orders[1]->customer instanceof Customer); + $this->assertEquals((string) $orders[1]->customer->_id, (string) $orders[1]->customer_id); + } } diff --git a/tests/unit/extensions/mongodb/CacheTest.php b/tests/unit/extensions/mongodb/CacheTest.php index ed85e95fab9..3b300778e2c 100644 --- a/tests/unit/extensions/mongodb/CacheTest.php +++ b/tests/unit/extensions/mongodb/CacheTest.php @@ -7,129 +7,129 @@ class CacheTest extends MongoDbTestCase { - /** - * @var string test cache collection name. - */ - protected static $cacheCollection = '_test_cache'; - - protected function tearDown() - { - $this->dropCollection(static::$cacheCollection); - parent::tearDown(); - } - - /** - * Creates test cache instance. - * @return Cache cache instance. - */ - protected function createCache() - { - return Yii::createObject([ - 'class' => Cache::className(), - 'db' => $this->getConnection(), - 'cacheCollection' => static::$cacheCollection, - 'gcProbability' => 0, - ]); - } - - // Tests: - - public function testSet() - { - $cache = $this->createCache(); - - $key = 'test_key'; - $value = 'test_value'; - $this->assertTrue($cache->set($key, $value), 'Unable to set value!'); - $this->assertEquals($value, $cache->get($key), 'Unable to set value correctly!'); - - $newValue = 'test_new_value'; - $this->assertTrue($cache->set($key, $newValue), 'Unable to update value!'); - $this->assertEquals($newValue, $cache->get($key), 'Unable to update value correctly!'); - } - - public function testAdd() - { - $cache = $this->createCache(); - - $key = 'test_key'; - $value = 'test_value'; - $this->assertTrue($cache->add($key, $value), 'Unable to add value!'); - $this->assertEquals($value, $cache->get($key), 'Unable to add value correctly!'); - - $newValue = 'test_new_value'; - $this->assertTrue($cache->add($key, $newValue), 'Unable to re-add value!'); - $this->assertEquals($value, $cache->get($key), 'Original value is lost!'); - } - - /** - * @depends testSet - */ - public function testDelete() - { - $cache = $this->createCache(); - - $key = 'test_key'; - $value = 'test_value'; - $cache->set($key, $value); - - $this->assertTrue($cache->delete($key), 'Unable to delete key!'); - $this->assertEquals(false, $cache->get($key), 'Value is not deleted!'); - } - - /** - * @depends testSet - */ - public function testFlush() - { - $cache = $this->createCache(); - - $cache->set('key1', 'value1'); - $cache->set('key2', 'value2'); - - $this->assertTrue($cache->flush(), 'Unable to flush cache!'); - - $collection = $cache->db->getCollection($cache->cacheCollection); - $rows = $this->findAll($collection); - $this->assertCount(0, $rows, 'Unable to flush records!'); - } - - /** - * @depends testSet - */ - public function testGc() - { - $cache = $this->createCache(); - - $cache->set('key1', 'value1'); - $cache->set('key2', 'value2'); - - $collection = $cache->db->getCollection($cache->cacheCollection); - - list($row) = $this->findAll($collection); - $collection->update(['_id' => $row['_id']], ['expire' => time() - 10]); - - $cache->gc(true); - - $rows = $this->findAll($collection); - $this->assertCount(1, $rows, 'Unable to collect garbage!'); - } - - /** - * @depends testSet - */ - public function testGetExpired() - { - $cache = $this->createCache(); - - $key = 'test_key'; - $value = 'test_value'; - $cache->set($key, $value); - - $collection = $cache->db->getCollection($cache->cacheCollection); - list($row) = $this->findAll($collection); - $collection->update(['_id' => $row['_id']], ['expire' => time() - 10]); - - $this->assertEquals(false, $cache->get($key), 'Expired key value returned!'); - } + /** + * @var string test cache collection name. + */ + protected static $cacheCollection = '_test_cache'; + + protected function tearDown() + { + $this->dropCollection(static::$cacheCollection); + parent::tearDown(); + } + + /** + * Creates test cache instance. + * @return Cache cache instance. + */ + protected function createCache() + { + return Yii::createObject([ + 'class' => Cache::className(), + 'db' => $this->getConnection(), + 'cacheCollection' => static::$cacheCollection, + 'gcProbability' => 0, + ]); + } + + // Tests: + + public function testSet() + { + $cache = $this->createCache(); + + $key = 'test_key'; + $value = 'test_value'; + $this->assertTrue($cache->set($key, $value), 'Unable to set value!'); + $this->assertEquals($value, $cache->get($key), 'Unable to set value correctly!'); + + $newValue = 'test_new_value'; + $this->assertTrue($cache->set($key, $newValue), 'Unable to update value!'); + $this->assertEquals($newValue, $cache->get($key), 'Unable to update value correctly!'); + } + + public function testAdd() + { + $cache = $this->createCache(); + + $key = 'test_key'; + $value = 'test_value'; + $this->assertTrue($cache->add($key, $value), 'Unable to add value!'); + $this->assertEquals($value, $cache->get($key), 'Unable to add value correctly!'); + + $newValue = 'test_new_value'; + $this->assertTrue($cache->add($key, $newValue), 'Unable to re-add value!'); + $this->assertEquals($value, $cache->get($key), 'Original value is lost!'); + } + + /** + * @depends testSet + */ + public function testDelete() + { + $cache = $this->createCache(); + + $key = 'test_key'; + $value = 'test_value'; + $cache->set($key, $value); + + $this->assertTrue($cache->delete($key), 'Unable to delete key!'); + $this->assertEquals(false, $cache->get($key), 'Value is not deleted!'); + } + + /** + * @depends testSet + */ + public function testFlush() + { + $cache = $this->createCache(); + + $cache->set('key1', 'value1'); + $cache->set('key2', 'value2'); + + $this->assertTrue($cache->flush(), 'Unable to flush cache!'); + + $collection = $cache->db->getCollection($cache->cacheCollection); + $rows = $this->findAll($collection); + $this->assertCount(0, $rows, 'Unable to flush records!'); + } + + /** + * @depends testSet + */ + public function testGc() + { + $cache = $this->createCache(); + + $cache->set('key1', 'value1'); + $cache->set('key2', 'value2'); + + $collection = $cache->db->getCollection($cache->cacheCollection); + + list($row) = $this->findAll($collection); + $collection->update(['_id' => $row['_id']], ['expire' => time() - 10]); + + $cache->gc(true); + + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'Unable to collect garbage!'); + } + + /** + * @depends testSet + */ + public function testGetExpired() + { + $cache = $this->createCache(); + + $key = 'test_key'; + $value = 'test_value'; + $cache->set($key, $value); + + $collection = $cache->db->getCollection($cache->cacheCollection); + list($row) = $this->findAll($collection); + $collection->update(['_id' => $row['_id']], ['expire' => time() - 10]); + + $this->assertEquals(false, $cache->get($key), 'Expired key value returned!'); + } } diff --git a/tests/unit/extensions/mongodb/CollectionTest.php b/tests/unit/extensions/mongodb/CollectionTest.php index cd50d8a33f2..f5d1034f9f0 100644 --- a/tests/unit/extensions/mongodb/CollectionTest.php +++ b/tests/unit/extensions/mongodb/CollectionTest.php @@ -7,460 +7,460 @@ */ class CollectionTest extends MongoDbTestCase { - protected function tearDown() - { - $this->dropCollection('customer'); - $this->dropCollection('mapReduceOut'); - parent::tearDown(); - } - - // Tests : - - public function testGetName() - { - $collectionName = 'customer'; - $collection = $this->getConnection()->getCollection($collectionName); - $this->assertEquals($collectionName, $collection->getName()); - $this->assertEquals($this->mongoDbConfig['defaultDatabaseName'] . '.' . $collectionName, $collection->getFullName()); - } - - public function testFind() - { - $collection = $this->getConnection()->getCollection('customer'); - $cursor = $collection->find(); - $this->assertTrue($cursor instanceof \MongoCursor); - } - - public function testInsert() - { - $collection = $this->getConnection()->getCollection('customer'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $id = $collection->insert($data); - $this->assertTrue($id instanceof \MongoId); - $this->assertNotEmpty($id->__toString()); - } - - /** - * @depends testInsert - * @depends testFind - */ - public function testFindAll() - { - $collection = $this->getConnection()->getCollection('customer'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $id = $collection->insert($data); - - $cursor = $collection->find(); - $rows = []; - foreach ($cursor as $row) { - $rows[] = $row; - } - $this->assertEquals(1, count($rows)); - $this->assertEquals($id, $rows[0]['_id']); - } - - /** - * @depends testFind - */ - public function testBatchInsert() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = [ - [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ], - [ - 'name' => 'customer 2', - 'address' => 'customer 2 address', - ], - ]; - $insertedRows = $collection->batchInsert($rows); - $this->assertTrue($insertedRows[0]['_id'] instanceof \MongoId); - $this->assertTrue($insertedRows[1]['_id'] instanceof \MongoId); - $this->assertEquals(count($rows), $collection->find()->count()); - } - - public function testSave() - { - $collection = $this->getConnection()->getCollection('customer'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $id = $collection->save($data); - $this->assertTrue($id instanceof \MongoId); - $this->assertNotEmpty($id->__toString()); - } - - /** - * @depends testSave - */ - public function testUpdateBySave() - { - $collection = $this->getConnection()->getCollection('customer'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $newId = $collection->save($data); - - $updatedId = $collection->save($data); - $this->assertEquals($newId, $updatedId, 'Unable to update data!'); - - $data['_id'] = $newId->__toString(); - $updatedId = $collection->save($data); - $this->assertEquals($newId, $updatedId, 'Unable to updated data by string id!'); - } - - /** - * @depends testFindAll - */ - public function testRemove() - { - $collection = $this->getConnection()->getCollection('customer'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $id = $collection->insert($data); - - $count = $collection->remove(['_id' => $id]); - $this->assertEquals(1, $count); - - $rows = $this->findAll($collection); - $this->assertEquals(0, count($rows)); - } - - /** - * @depends testFindAll - */ - public function testUpdate() - { - $collection = $this->getConnection()->getCollection('customer'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $id = $collection->insert($data); - - $newData = [ - 'name' => 'new name' - ]; - $count = $collection->update(['_id' => $id], $newData); - $this->assertEquals(1, $count); - - list($row) = $this->findAll($collection); - $this->assertEquals($newData['name'], $row['name']); - } - - /** - * @depends testBatchInsert - */ - public function testGroup() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = [ - [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ], - [ - 'name' => 'customer 2', - 'address' => 'customer 2 address', - ], - ]; - $collection->batchInsert($rows); - - $keys = ['address' => 1]; - $initial = ['items' => []]; - $reduce = "function (obj, prev) { prev.items.push(obj.name); }"; - $result = $collection->group($keys, $initial, $reduce); - $this->assertEquals(2, count($result)); - $this->assertNotEmpty($result[0]['address']); - $this->assertNotEmpty($result[0]['items']); - } - - public function testFindAndModify() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = [ - [ - 'name' => 'customer 1', - 'status' => 1, - 'amount' => 100, - ], - [ - 'name' => 'customer 2', - 'status' => 1, - 'amount' => 200, - ], - ]; - $collection->batchInsert($rows); - - // increment field - $result = $collection->findAndModify(['name' => 'customer 1'], ['$inc' => ['status' => 1]]); - $this->assertEquals('customer 1', $result['name']); - $this->assertEquals(1, $result['status']); - $newResult = $collection->findOne(['name' => 'customer 1']); - $this->assertEquals(2, $newResult['status']); - - // $set and return modified document - $result = $collection->findAndModify( - ['name' => 'customer 2'], - ['$set' => ['status' => 2]], - [], - ['new' => true] - ); - $this->assertEquals('customer 2', $result['name']); - $this->assertEquals(2, $result['status']); - - // Full update document - $data = [ - 'name' => 'customer 3', - 'city' => 'Minsk' - ]; - $result = $collection->findAndModify( - ['name' => 'customer 2'], - $data, - [], - ['new' => true] - ); - $this->assertEquals('customer 3', $result['name']); - $this->assertEquals('Minsk', $result['city']); - $this->assertTrue(!isset($result['status'])); - - // Test exceptions - $this->setExpectedException('\yii\mongodb\Exception'); - $collection->findAndModify(['name' => 'customer 1'], ['$wrongOperator' => ['status' => 1]]); - } - - /** - * @depends testBatchInsert - */ - public function testMapReduce() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = [ - [ - 'name' => 'customer 1', - 'status' => 1, - 'amount' => 100, - ], - [ - 'name' => 'customer 2', - 'status' => 1, - 'amount' => 200, - ], - [ - 'name' => 'customer 2', - 'status' => 2, - 'amount' => 400, - ], - [ - 'name' => 'customer 2', - 'status' => 3, - 'amount' => 500, - ], - ]; - $collection->batchInsert($rows); - - $result = $collection->mapReduce( - 'function () {emit(this.status, this.amount)}', - 'function (key, values) {return Array.sum(values)}', - 'mapReduceOut', - ['status' => ['$lt' => 3]] - ); - $this->assertEquals('mapReduceOut', $result); - - $outputCollection = $this->getConnection()->getCollection($result); - $rows = $this->findAll($outputCollection); - $expectedRows = [ - [ - '_id' => 1, - 'value' => 300, - ], - [ - '_id' => 2, - 'value' => 400, - ], - ]; - $this->assertEquals($expectedRows, $rows); - } - - /** - * @depends testMapReduce - */ - public function testMapReduceInline() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = [ - [ - 'name' => 'customer 1', - 'status' => 1, - 'amount' => 100, - ], - [ - 'name' => 'customer 2', - 'status' => 1, - 'amount' => 200, - ], - [ - 'name' => 'customer 2', - 'status' => 2, - 'amount' => 400, - ], - [ - 'name' => 'customer 2', - 'status' => 3, - 'amount' => 500, - ], - ]; - $collection->batchInsert($rows); - - $result = $collection->mapReduce( - 'function () {emit(this.status, this.amount)}', - 'function (key, values) {return Array.sum(values)}', - ['inline' => true], - ['status' => ['$lt' => 3]] - ); - $expectedRows = [ - [ - '_id' => 1, - 'value' => 300, - ], - [ - '_id' => 2, - 'value' => 400, - ], - ]; - $this->assertEquals($expectedRows, $result); - } - - public function testCreateIndex() - { - $collection = $this->getConnection()->getCollection('customer'); - $columns = [ - 'name', - 'status' => \MongoCollection::DESCENDING, - ]; - $this->assertTrue($collection->createIndex($columns)); - $indexInfo = $collection->mongoCollection->getIndexInfo(); - $this->assertEquals(2, count($indexInfo)); - } - - /** - * @depends testCreateIndex - */ - public function testDropIndex() - { - $collection = $this->getConnection()->getCollection('customer'); - - $collection->createIndex('name'); - $this->assertTrue($collection->dropIndex('name')); - $indexInfo = $collection->mongoCollection->getIndexInfo(); - $this->assertEquals(1, count($indexInfo)); - - $this->setExpectedException('\yii\mongodb\Exception'); - $collection->dropIndex('name'); - } - - /** - * @depends testCreateIndex - */ - public function testDropAllIndexes() - { - $collection = $this->getConnection()->getCollection('customer'); - $collection->createIndex('name'); - $this->assertEquals(2, $collection->dropAllIndexes()); - $indexInfo = $collection->mongoCollection->getIndexInfo(); - $this->assertEquals(1, count($indexInfo)); - } - - /** - * @depends testBatchInsert - * @depends testCreateIndex - */ - public function testFullTextSearch() - { - if (version_compare('2.4', $this->getServerVersion(), '>')) { - $this->markTestSkipped("Mongo Server 2.4 required."); - } - - $collection = $this->getConnection()->getCollection('customer'); - - $rows = [ - [ - 'name' => 'customer 1', - 'status' => 1, - 'amount' => 100, - ], - [ - 'name' => 'some customer', - 'status' => 1, - 'amount' => 200, - ], - [ - 'name' => 'no search keyword', - 'status' => 1, - 'amount' => 200, - ], - ]; - $collection->batchInsert($rows); - $collection->createIndex(['name' => 'text']); - - $result = $collection->fullTextSearch('customer'); - $this->assertNotEmpty($result); - $this->assertCount(2, $result); - } - - /** - * @depends testInsert - * @depends testFind - */ - public function testFindByNotObjectId() - { - $collection = $this->getConnection()->getCollection('customer'); - - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - ]; - $id = $collection->insert($data); - - $cursor = $collection->find(['_id' => (string)$id]); - $this->assertTrue($cursor instanceof \MongoCursor); - $row = $cursor->getNext(); - $this->assertEquals($id, $row['_id']); - - $cursor = $collection->find(['_id' => 'fake']); - $this->assertTrue($cursor instanceof \MongoCursor); - $this->assertEquals(0, $cursor->count()); - } - - /** - * @depends testInsert - * - * @see https://github.com/yiisoft/yii2/issues/2548 - */ - public function testInsertMongoBin() - { - $collection = $this->getConnection()->getCollection('customer'); - - $fileName = realpath(__DIR__ . '/../../../../extensions/gii/assets/logo.png'); - $data = [ - 'name' => 'customer 1', - 'address' => 'customer 1 address', - 'binData' => new \MongoBinData(file_get_contents($fileName), 2), - ]; - $id = $collection->insert($data); - $this->assertTrue($id instanceof \MongoId); - $this->assertNotEmpty($id->__toString()); - } + protected function tearDown() + { + $this->dropCollection('customer'); + $this->dropCollection('mapReduceOut'); + parent::tearDown(); + } + + // Tests : + + public function testGetName() + { + $collectionName = 'customer'; + $collection = $this->getConnection()->getCollection($collectionName); + $this->assertEquals($collectionName, $collection->getName()); + $this->assertEquals($this->mongoDbConfig['defaultDatabaseName'] . '.' . $collectionName, $collection->getFullName()); + } + + public function testFind() + { + $collection = $this->getConnection()->getCollection('customer'); + $cursor = $collection->find(); + $this->assertTrue($cursor instanceof \MongoCursor); + } + + public function testInsert() + { + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $id = $collection->insert($data); + $this->assertTrue($id instanceof \MongoId); + $this->assertNotEmpty($id->__toString()); + } + + /** + * @depends testInsert + * @depends testFind + */ + public function testFindAll() + { + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $id = $collection->insert($data); + + $cursor = $collection->find(); + $rows = []; + foreach ($cursor as $row) { + $rows[] = $row; + } + $this->assertEquals(1, count($rows)); + $this->assertEquals($id, $rows[0]['_id']); + } + + /** + * @depends testFind + */ + public function testBatchInsert() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ], + [ + 'name' => 'customer 2', + 'address' => 'customer 2 address', + ], + ]; + $insertedRows = $collection->batchInsert($rows); + $this->assertTrue($insertedRows[0]['_id'] instanceof \MongoId); + $this->assertTrue($insertedRows[1]['_id'] instanceof \MongoId); + $this->assertEquals(count($rows), $collection->find()->count()); + } + + public function testSave() + { + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $id = $collection->save($data); + $this->assertTrue($id instanceof \MongoId); + $this->assertNotEmpty($id->__toString()); + } + + /** + * @depends testSave + */ + public function testUpdateBySave() + { + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $newId = $collection->save($data); + + $updatedId = $collection->save($data); + $this->assertEquals($newId, $updatedId, 'Unable to update data!'); + + $data['_id'] = $newId->__toString(); + $updatedId = $collection->save($data); + $this->assertEquals($newId, $updatedId, 'Unable to updated data by string id!'); + } + + /** + * @depends testFindAll + */ + public function testRemove() + { + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $id = $collection->insert($data); + + $count = $collection->remove(['_id' => $id]); + $this->assertEquals(1, $count); + + $rows = $this->findAll($collection); + $this->assertEquals(0, count($rows)); + } + + /** + * @depends testFindAll + */ + public function testUpdate() + { + $collection = $this->getConnection()->getCollection('customer'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $id = $collection->insert($data); + + $newData = [ + 'name' => 'new name' + ]; + $count = $collection->update(['_id' => $id], $newData); + $this->assertEquals(1, $count); + + list($row) = $this->findAll($collection); + $this->assertEquals($newData['name'], $row['name']); + } + + /** + * @depends testBatchInsert + */ + public function testGroup() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ], + [ + 'name' => 'customer 2', + 'address' => 'customer 2 address', + ], + ]; + $collection->batchInsert($rows); + + $keys = ['address' => 1]; + $initial = ['items' => []]; + $reduce = "function (obj, prev) { prev.items.push(obj.name); }"; + $result = $collection->group($keys, $initial, $reduce); + $this->assertEquals(2, count($result)); + $this->assertNotEmpty($result[0]['address']); + $this->assertNotEmpty($result[0]['items']); + } + + public function testFindAndModify() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'customer 2', + 'status' => 1, + 'amount' => 200, + ], + ]; + $collection->batchInsert($rows); + + // increment field + $result = $collection->findAndModify(['name' => 'customer 1'], ['$inc' => ['status' => 1]]); + $this->assertEquals('customer 1', $result['name']); + $this->assertEquals(1, $result['status']); + $newResult = $collection->findOne(['name' => 'customer 1']); + $this->assertEquals(2, $newResult['status']); + + // $set and return modified document + $result = $collection->findAndModify( + ['name' => 'customer 2'], + ['$set' => ['status' => 2]], + [], + ['new' => true] + ); + $this->assertEquals('customer 2', $result['name']); + $this->assertEquals(2, $result['status']); + + // Full update document + $data = [ + 'name' => 'customer 3', + 'city' => 'Minsk' + ]; + $result = $collection->findAndModify( + ['name' => 'customer 2'], + $data, + [], + ['new' => true] + ); + $this->assertEquals('customer 3', $result['name']); + $this->assertEquals('Minsk', $result['city']); + $this->assertTrue(!isset($result['status'])); + + // Test exceptions + $this->setExpectedException('\yii\mongodb\Exception'); + $collection->findAndModify(['name' => 'customer 1'], ['$wrongOperator' => ['status' => 1]]); + } + + /** + * @depends testBatchInsert + */ + public function testMapReduce() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'customer 2', + 'status' => 1, + 'amount' => 200, + ], + [ + 'name' => 'customer 2', + 'status' => 2, + 'amount' => 400, + ], + [ + 'name' => 'customer 2', + 'status' => 3, + 'amount' => 500, + ], + ]; + $collection->batchInsert($rows); + + $result = $collection->mapReduce( + 'function () {emit(this.status, this.amount)}', + 'function (key, values) {return Array.sum(values)}', + 'mapReduceOut', + ['status' => ['$lt' => 3]] + ); + $this->assertEquals('mapReduceOut', $result); + + $outputCollection = $this->getConnection()->getCollection($result); + $rows = $this->findAll($outputCollection); + $expectedRows = [ + [ + '_id' => 1, + 'value' => 300, + ], + [ + '_id' => 2, + 'value' => 400, + ], + ]; + $this->assertEquals($expectedRows, $rows); + } + + /** + * @depends testMapReduce + */ + public function testMapReduceInline() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'customer 2', + 'status' => 1, + 'amount' => 200, + ], + [ + 'name' => 'customer 2', + 'status' => 2, + 'amount' => 400, + ], + [ + 'name' => 'customer 2', + 'status' => 3, + 'amount' => 500, + ], + ]; + $collection->batchInsert($rows); + + $result = $collection->mapReduce( + 'function () {emit(this.status, this.amount)}', + 'function (key, values) {return Array.sum(values)}', + ['inline' => true], + ['status' => ['$lt' => 3]] + ); + $expectedRows = [ + [ + '_id' => 1, + 'value' => 300, + ], + [ + '_id' => 2, + 'value' => 400, + ], + ]; + $this->assertEquals($expectedRows, $result); + } + + public function testCreateIndex() + { + $collection = $this->getConnection()->getCollection('customer'); + $columns = [ + 'name', + 'status' => \MongoCollection::DESCENDING, + ]; + $this->assertTrue($collection->createIndex($columns)); + $indexInfo = $collection->mongoCollection->getIndexInfo(); + $this->assertEquals(2, count($indexInfo)); + } + + /** + * @depends testCreateIndex + */ + public function testDropIndex() + { + $collection = $this->getConnection()->getCollection('customer'); + + $collection->createIndex('name'); + $this->assertTrue($collection->dropIndex('name')); + $indexInfo = $collection->mongoCollection->getIndexInfo(); + $this->assertEquals(1, count($indexInfo)); + + $this->setExpectedException('\yii\mongodb\Exception'); + $collection->dropIndex('name'); + } + + /** + * @depends testCreateIndex + */ + public function testDropAllIndexes() + { + $collection = $this->getConnection()->getCollection('customer'); + $collection->createIndex('name'); + $this->assertEquals(2, $collection->dropAllIndexes()); + $indexInfo = $collection->mongoCollection->getIndexInfo(); + $this->assertEquals(1, count($indexInfo)); + } + + /** + * @depends testBatchInsert + * @depends testCreateIndex + */ + public function testFullTextSearch() + { + if (version_compare('2.4', $this->getServerVersion(), '>')) { + $this->markTestSkipped("Mongo Server 2.4 required."); + } + + $collection = $this->getConnection()->getCollection('customer'); + + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'some customer', + 'status' => 1, + 'amount' => 200, + ], + [ + 'name' => 'no search keyword', + 'status' => 1, + 'amount' => 200, + ], + ]; + $collection->batchInsert($rows); + $collection->createIndex(['name' => 'text']); + + $result = $collection->fullTextSearch('customer'); + $this->assertNotEmpty($result); + $this->assertCount(2, $result); + } + + /** + * @depends testInsert + * @depends testFind + */ + public function testFindByNotObjectId() + { + $collection = $this->getConnection()->getCollection('customer'); + + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + ]; + $id = $collection->insert($data); + + $cursor = $collection->find(['_id' => (string) $id]); + $this->assertTrue($cursor instanceof \MongoCursor); + $row = $cursor->getNext(); + $this->assertEquals($id, $row['_id']); + + $cursor = $collection->find(['_id' => 'fake']); + $this->assertTrue($cursor instanceof \MongoCursor); + $this->assertEquals(0, $cursor->count()); + } + + /** + * @depends testInsert + * + * @see https://github.com/yiisoft/yii2/issues/2548 + */ + public function testInsertMongoBin() + { + $collection = $this->getConnection()->getCollection('customer'); + + $fileName = realpath(__DIR__ . '/../../../../extensions/gii/assets/logo.png'); + $data = [ + 'name' => 'customer 1', + 'address' => 'customer 1 address', + 'binData' => new \MongoBinData(file_get_contents($fileName), 2), + ]; + $id = $collection->insert($data); + $this->assertTrue($id instanceof \MongoId); + $this->assertNotEmpty($id->__toString()); + } } diff --git a/tests/unit/extensions/mongodb/ConnectionTest.php b/tests/unit/extensions/mongodb/ConnectionTest.php index 5acd88d5b27..72e1c88b187 100644 --- a/tests/unit/extensions/mongodb/ConnectionTest.php +++ b/tests/unit/extensions/mongodb/ConnectionTest.php @@ -12,108 +12,108 @@ */ class ConnectionTest extends MongoDbTestCase { - public function testConstruct() - { - $connection = $this->getConnection(false); - $params = $this->mongoDbConfig; - - $connection->open(); - - $this->assertEquals($params['dsn'], $connection->dsn); - $this->assertEquals($params['defaultDatabaseName'], $connection->defaultDatabaseName); - $this->assertEquals($params['options'], $connection->options); - } - - public function testOpenClose() - { - $connection = $this->getConnection(false, false); - - $this->assertFalse($connection->isActive); - $this->assertEquals(null, $connection->mongoClient); - - $connection->open(); - $this->assertTrue($connection->isActive); - $this->assertTrue(is_object($connection->mongoClient)); - - $connection->close(); - $this->assertFalse($connection->isActive); - $this->assertEquals(null, $connection->mongoClient); - - $connection = new Connection; - $connection->dsn = 'unknown::memory:'; - $this->setExpectedException('yii\mongodb\Exception'); - $connection->open(); - } - - public function testGetDatabase() - { - $connection = $this->getConnection(); - - $database = $connection->getDatabase($connection->defaultDatabaseName); - $this->assertTrue($database instanceof Database); - $this->assertTrue($database->mongoDb instanceof \MongoDB); - - $database2 = $connection->getDatabase($connection->defaultDatabaseName); - $this->assertTrue($database === $database2); - - $databaseRefreshed = $connection->getDatabase($connection->defaultDatabaseName, true); - $this->assertFalse($database === $databaseRefreshed); - } - - /** - * @depends testGetDatabase - */ - public function testGetDefaultDatabase() - { - $connection = new Connection(); - $connection->dsn = $this->mongoDbConfig['dsn']; - $connection->defaultDatabaseName = $this->mongoDbConfig['defaultDatabaseName']; - $database = $connection->getDatabase(); - $this->assertTrue($database instanceof Database, 'Unable to get default database!'); - - $connection = new Connection(); - $connection->dsn = $this->mongoDbConfig['dsn']; - $connection->options = ['db' => $this->mongoDbConfig['defaultDatabaseName']]; - $database = $connection->getDatabase(); - $this->assertTrue($database instanceof Database, 'Unable to determine default database from options!'); - - $connection = new Connection(); - $connection->dsn = $this->mongoDbConfig['dsn'] . '/' . $this->mongoDbConfig['defaultDatabaseName']; - $database = $connection->getDatabase(); - $this->assertTrue($database instanceof Database, 'Unable to determine default database from dsn!'); - } - - /** - * @depends testGetDefaultDatabase - */ - public function testGetCollection() - { - $connection = $this->getConnection(); - - $collection = $connection->getCollection('customer'); - $this->assertTrue($collection instanceof Collection); - - $collection2 = $connection->getCollection('customer'); - $this->assertTrue($collection === $collection2); - - $collection2 = $connection->getCollection('customer', true); - $this->assertFalse($collection === $collection2); - } - - /** - * @depends testGetDefaultDatabase - */ - public function testGetFileCollection() - { - $connection = $this->getConnection(); - - $collection = $connection->getFileCollection('testfs'); - $this->assertTrue($collection instanceof FileCollection); - - $collection2 = $connection->getFileCollection('testfs'); - $this->assertTrue($collection === $collection2); - - $collection2 = $connection->getFileCollection('testfs', true); - $this->assertFalse($collection === $collection2); - } + public function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->mongoDbConfig; + + $connection->open(); + + $this->assertEquals($params['dsn'], $connection->dsn); + $this->assertEquals($params['defaultDatabaseName'], $connection->defaultDatabaseName); + $this->assertEquals($params['options'], $connection->options); + } + + public function testOpenClose() + { + $connection = $this->getConnection(false, false); + + $this->assertFalse($connection->isActive); + $this->assertEquals(null, $connection->mongoClient); + + $connection->open(); + $this->assertTrue($connection->isActive); + $this->assertTrue(is_object($connection->mongoClient)); + + $connection->close(); + $this->assertFalse($connection->isActive); + $this->assertEquals(null, $connection->mongoClient); + + $connection = new Connection; + $connection->dsn = 'unknown::memory:'; + $this->setExpectedException('yii\mongodb\Exception'); + $connection->open(); + } + + public function testGetDatabase() + { + $connection = $this->getConnection(); + + $database = $connection->getDatabase($connection->defaultDatabaseName); + $this->assertTrue($database instanceof Database); + $this->assertTrue($database->mongoDb instanceof \MongoDB); + + $database2 = $connection->getDatabase($connection->defaultDatabaseName); + $this->assertTrue($database === $database2); + + $databaseRefreshed = $connection->getDatabase($connection->defaultDatabaseName, true); + $this->assertFalse($database === $databaseRefreshed); + } + + /** + * @depends testGetDatabase + */ + public function testGetDefaultDatabase() + { + $connection = new Connection(); + $connection->dsn = $this->mongoDbConfig['dsn']; + $connection->defaultDatabaseName = $this->mongoDbConfig['defaultDatabaseName']; + $database = $connection->getDatabase(); + $this->assertTrue($database instanceof Database, 'Unable to get default database!'); + + $connection = new Connection(); + $connection->dsn = $this->mongoDbConfig['dsn']; + $connection->options = ['db' => $this->mongoDbConfig['defaultDatabaseName']]; + $database = $connection->getDatabase(); + $this->assertTrue($database instanceof Database, 'Unable to determine default database from options!'); + + $connection = new Connection(); + $connection->dsn = $this->mongoDbConfig['dsn'] . '/' . $this->mongoDbConfig['defaultDatabaseName']; + $database = $connection->getDatabase(); + $this->assertTrue($database instanceof Database, 'Unable to determine default database from dsn!'); + } + + /** + * @depends testGetDefaultDatabase + */ + public function testGetCollection() + { + $connection = $this->getConnection(); + + $collection = $connection->getCollection('customer'); + $this->assertTrue($collection instanceof Collection); + + $collection2 = $connection->getCollection('customer'); + $this->assertTrue($collection === $collection2); + + $collection2 = $connection->getCollection('customer', true); + $this->assertFalse($collection === $collection2); + } + + /** + * @depends testGetDefaultDatabase + */ + public function testGetFileCollection() + { + $connection = $this->getConnection(); + + $collection = $connection->getFileCollection('testfs'); + $this->assertTrue($collection instanceof FileCollection); + + $collection2 = $connection->getFileCollection('testfs'); + $this->assertTrue($collection === $collection2); + + $collection2 = $connection->getFileCollection('testfs', true); + $this->assertFalse($collection === $collection2); + } } diff --git a/tests/unit/extensions/mongodb/DatabaseTest.php b/tests/unit/extensions/mongodb/DatabaseTest.php index f448be00ecb..f4bc5428121 100644 --- a/tests/unit/extensions/mongodb/DatabaseTest.php +++ b/tests/unit/extensions/mongodb/DatabaseTest.php @@ -10,61 +10,61 @@ */ class DatabaseTest extends MongoDbTestCase { - protected function tearDown() - { - $this->dropCollection('customer'); - $this->dropFileCollection('testfs'); - parent::tearDown(); - } + protected function tearDown() + { + $this->dropCollection('customer'); + $this->dropFileCollection('testfs'); + parent::tearDown(); + } - // Tests : + // Tests : - public function testGetCollection() - { - $database = $connection = $this->getConnection()->getDatabase(); + public function testGetCollection() + { + $database = $connection = $this->getConnection()->getDatabase(); - $collection = $database->getCollection('customer'); - $this->assertTrue($collection instanceof Collection); - $this->assertTrue($collection->mongoCollection instanceof \MongoCollection); + $collection = $database->getCollection('customer'); + $this->assertTrue($collection instanceof Collection); + $this->assertTrue($collection->mongoCollection instanceof \MongoCollection); - $collection2 = $database->getCollection('customer'); - $this->assertTrue($collection === $collection2); + $collection2 = $database->getCollection('customer'); + $this->assertTrue($collection === $collection2); - $collectionRefreshed = $database->getCollection('customer', true); - $this->assertFalse($collection === $collectionRefreshed); - } + $collectionRefreshed = $database->getCollection('customer', true); + $this->assertFalse($collection === $collectionRefreshed); + } - public function testGetFileCollection() - { - $database = $connection = $this->getConnection()->getDatabase(); + public function testGetFileCollection() + { + $database = $connection = $this->getConnection()->getDatabase(); - $collection = $database->getFileCollection('testfs'); - $this->assertTrue($collection instanceof FileCollection); - $this->assertTrue($collection->mongoCollection instanceof \MongoGridFS); + $collection = $database->getFileCollection('testfs'); + $this->assertTrue($collection instanceof FileCollection); + $this->assertTrue($collection->mongoCollection instanceof \MongoGridFS); - $collection2 = $database->getFileCollection('testfs'); - $this->assertTrue($collection === $collection2); + $collection2 = $database->getFileCollection('testfs'); + $this->assertTrue($collection === $collection2); - $collectionRefreshed = $database->getFileCollection('testfs', true); - $this->assertFalse($collection === $collectionRefreshed); - } + $collectionRefreshed = $database->getFileCollection('testfs', true); + $this->assertFalse($collection === $collectionRefreshed); + } - public function testExecuteCommand() - { - $database = $connection = $this->getConnection()->getDatabase(); + public function testExecuteCommand() + { + $database = $connection = $this->getConnection()->getDatabase(); - $result = $database->executeCommand([ - 'distinct' => 'customer', - 'key' => 'name' - ]); - $this->assertTrue(array_key_exists('ok', $result)); - $this->assertTrue(array_key_exists('values', $result)); - } + $result = $database->executeCommand([ + 'distinct' => 'customer', + 'key' => 'name' + ]); + $this->assertTrue(array_key_exists('ok', $result)); + $this->assertTrue(array_key_exists('values', $result)); + } - public function testCreateCollection() - { - $database = $connection = $this->getConnection()->getDatabase(); - $collection = $database->createCollection('customer'); - $this->assertTrue($collection instanceof \MongoCollection); - } + public function testCreateCollection() + { + $database = $connection = $this->getConnection()->getDatabase(); + $collection = $database->createCollection('customer'); + $this->assertTrue($collection instanceof \MongoCollection); + } } diff --git a/tests/unit/extensions/mongodb/MongoDbTestCase.php b/tests/unit/extensions/mongodb/MongoDbTestCase.php index 809d1b98615..669bf589099 100644 --- a/tests/unit/extensions/mongodb/MongoDbTestCase.php +++ b/tests/unit/extensions/mongodb/MongoDbTestCase.php @@ -10,140 +10,143 @@ class MongoDbTestCase extends TestCase { - /** - * @var array Mongo connection configuration. - */ - protected $mongoDbConfig = [ - 'dsn' => 'mongodb://localhost:27017', - 'defaultDatabaseName' => 'yii2test', - 'options' => [], - ]; - /** - * @var Connection Mongo connection instance. - */ - protected $mongodb; + /** + * @var array Mongo connection configuration. + */ + protected $mongoDbConfig = [ + 'dsn' => 'mongodb://localhost:27017', + 'defaultDatabaseName' => 'yii2test', + 'options' => [], + ]; + /** + * @var Connection Mongo connection instance. + */ + protected $mongodb; - public static function setUpBeforeClass() - { - static::loadClassMap(); - } + public static function setUpBeforeClass() + { + static::loadClassMap(); + } - protected function setUp() - { - parent::setUp(); - if (!extension_loaded('mongo')) { - $this->markTestSkipped('mongo extension required.'); - } - $config = $this->getParam('mongodb'); - if (!empty($config)) { - $this->mongoDbConfig = $config; - } - $this->mockApplication(); - static::loadClassMap(); - } + protected function setUp() + { + parent::setUp(); + if (!extension_loaded('mongo')) { + $this->markTestSkipped('mongo extension required.'); + } + $config = $this->getParam('mongodb'); + if (!empty($config)) { + $this->mongoDbConfig = $config; + } + $this->mockApplication(); + static::loadClassMap(); + } - protected function tearDown() - { - if ($this->mongodb) { - $this->mongodb->close(); - } - $this->destroyApplication(); - } + protected function tearDown() + { + if ($this->mongodb) { + $this->mongodb->close(); + } + $this->destroyApplication(); + } - /** - * Adds sphinx extension files to [[Yii::$classPath]], - * avoiding the necessity of usage Composer autoloader. - */ - protected static function loadClassMap() - { - $baseNameSpace = 'yii/mongodb'; - $basePath = realpath(__DIR__. '/../../../../extensions/mongodb'); - $files = FileHelper::findFiles($basePath); - foreach ($files as $file) { - $classRelativePath = str_replace($basePath, '', $file); - $classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath); - Yii::$classMap[$classFullName] = $file; - } - } + /** + * Adds sphinx extension files to [[Yii::$classPath]], + * avoiding the necessity of usage Composer autoloader. + */ + protected static function loadClassMap() + { + $baseNameSpace = 'yii/mongodb'; + $basePath = realpath(__DIR__. '/../../../../extensions/mongodb'); + $files = FileHelper::findFiles($basePath); + foreach ($files as $file) { + $classRelativePath = str_replace($basePath, '', $file); + $classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath); + Yii::$classMap[$classFullName] = $file; + } + } - /** - * @param boolean $reset whether to clean up the test database - * @param boolean $open whether to open test database - * @return \yii\mongodb\Connection - */ - public function getConnection($reset = false, $open = true) - { - if (!$reset && $this->mongodb) { - return $this->mongodb; - } - $db = new Connection; - $db->dsn = $this->mongoDbConfig['dsn']; - $db->defaultDatabaseName = $this->mongoDbConfig['defaultDatabaseName']; - if (isset($this->mongoDbConfig['options'])) { - $db->options = $this->mongoDbConfig['options']; - } - if ($open) { - $db->open(); - } - $this->mongodb = $db; - return $db; - } + /** + * @param boolean $reset whether to clean up the test database + * @param boolean $open whether to open test database + * @return \yii\mongodb\Connection + */ + public function getConnection($reset = false, $open = true) + { + if (!$reset && $this->mongodb) { + return $this->mongodb; + } + $db = new Connection; + $db->dsn = $this->mongoDbConfig['dsn']; + $db->defaultDatabaseName = $this->mongoDbConfig['defaultDatabaseName']; + if (isset($this->mongoDbConfig['options'])) { + $db->options = $this->mongoDbConfig['options']; + } + if ($open) { + $db->open(); + } + $this->mongodb = $db; - /** - * Drops the specified collection. - * @param string $name collection name. - */ - protected function dropCollection($name) - { - if ($this->mongodb) { - try { - $this->mongodb->getCollection($name)->drop(); - } catch (Exception $e) { - // shut down exception - } - } - } + return $db; + } - /** - * Drops the specified file collection. - * @param string $name file collection name. - */ - protected function dropFileCollection($name = 'fs') - { - if ($this->mongodb) { - try { - $this->mongodb->getFileCollection($name)->drop(); - } catch (Exception $e) { - // shut down exception - } - } - } + /** + * Drops the specified collection. + * @param string $name collection name. + */ + protected function dropCollection($name) + { + if ($this->mongodb) { + try { + $this->mongodb->getCollection($name)->drop(); + } catch (Exception $e) { + // shut down exception + } + } + } - /** - * Finds all records in collection. - * @param \yii\mongodb\Collection $collection - * @param array $condition - * @param array $fields - * @return array rows - */ - protected function findAll($collection, $condition = [], $fields = []) - { - $cursor = $collection->find($condition, $fields); - $result = []; - foreach ($cursor as $data) { - $result[] = $data; - } - return $result; - } + /** + * Drops the specified file collection. + * @param string $name file collection name. + */ + protected function dropFileCollection($name = 'fs') + { + if ($this->mongodb) { + try { + $this->mongodb->getFileCollection($name)->drop(); + } catch (Exception $e) { + // shut down exception + } + } + } - /** - * Returns the Mongo server version. - * @return string Mongo server version. - */ - protected function getServerVersion() - { - $connection = $this->getConnection(); - $buildInfo = $connection->getDatabase()->executeCommand(['buildinfo' => true]); - return $buildInfo['version']; - } + /** + * Finds all records in collection. + * @param \yii\mongodb\Collection $collection + * @param array $condition + * @param array $fields + * @return array rows + */ + protected function findAll($collection, $condition = [], $fields = []) + { + $cursor = $collection->find($condition, $fields); + $result = []; + foreach ($cursor as $data) { + $result[] = $data; + } + + return $result; + } + + /** + * Returns the Mongo server version. + * @return string Mongo server version. + */ + protected function getServerVersion() + { + $connection = $this->getConnection(); + $buildInfo = $connection->getDatabase()->executeCommand(['buildinfo' => true]); + + return $buildInfo['version']; + } } diff --git a/tests/unit/extensions/mongodb/QueryRunTest.php b/tests/unit/extensions/mongodb/QueryRunTest.php index 79c046126c6..0ac83132ac2 100644 --- a/tests/unit/extensions/mongodb/QueryRunTest.php +++ b/tests/unit/extensions/mongodb/QueryRunTest.php @@ -9,136 +9,136 @@ */ class QueryRunTest extends MongoDbTestCase { - protected function setUp() - { - parent::setUp(); - $this->setUpTestRows(); - } - - protected function tearDown() - { - $this->dropCollection('customer'); - parent::tearDown(); - } - - /** - * Sets up test rows. - */ - protected function setUpTestRows() - { - $collection = $this->getConnection()->getCollection('customer'); - $rows = []; - for ($i = 1; $i <= 10; $i++) { - $rows[] = [ - 'name' => 'name' . $i, - 'address' => 'address' . $i, - 'avatar' => [ - 'width' => 50 + $i, - 'height' => 100 + $i, - 'url' => 'http://some.url/' . $i, - ], - ]; - } - $collection->batchInsert($rows); - } - - // Tests : - - public function testAll() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer')->all($connection); - $this->assertEquals(10, count($rows)); - } - - public function testDirectMatch() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer') - ->where(['name' => 'name1']) - ->all($connection); - $this->assertEquals(1, count($rows)); - $this->assertEquals('name1', $rows[0]['name']); - } - - public function testIndexBy() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer') - ->indexBy('name') - ->all($connection); - $this->assertEquals(10, count($rows)); - $this->assertNotEmpty($rows['name1']); - } - - public function testInCondition() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer') - ->where([ - 'name' => ['name1', 'name5'] - ]) - ->all($connection); - $this->assertEquals(2, count($rows)); - $this->assertEquals('name1', $rows[0]['name']); - $this->assertEquals('name5', $rows[1]['name']); - } - - public function testOrCondition() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer') - ->where(['name' => 'name1']) - ->orWhere(['address' => 'address5']) - ->all($connection); - $this->assertEquals(2, count($rows)); - $this->assertEquals('name1', $rows[0]['name']); - $this->assertEquals('address5', $rows[1]['address']); - } - - public function testOrder() - { - $connection = $this->getConnection(); - - $query = new Query; - $rows = $query->from('customer') - ->orderBy(['name' => SORT_DESC]) - ->all($connection); - $this->assertEquals('name9', $rows[0]['name']); - - $query = new Query; - $rows = $query->from('customer') - ->orderBy(['avatar.height' => SORT_DESC]) - ->all($connection); - $this->assertEquals('name10', $rows[0]['name']); - } - - public function testMatchPlainId() - { - $connection = $this->getConnection(); - $query = new Query; - $row = $query->from('customer')->one($connection); - $query = new Query; - $rows = $query->from('customer') - ->where(['_id' => $row['_id']->__toString()]) - ->all($connection); - $this->assertEquals(1, count($rows)); - } - - public function testLike() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer') - ->where(['LIKE', 'name', '/me1/']) - ->all($connection); - $this->assertEquals(2, count($rows)); - $this->assertEquals('name1', $rows[0]['name']); - $this->assertEquals('name10', $rows[1]['name']); - } + protected function setUp() + { + parent::setUp(); + $this->setUpTestRows(); + } + + protected function tearDown() + { + $this->dropCollection('customer'); + parent::tearDown(); + } + + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = []; + for ($i = 1; $i <= 10; $i++) { + $rows[] = [ + 'name' => 'name' . $i, + 'address' => 'address' . $i, + 'avatar' => [ + 'width' => 50 + $i, + 'height' => 100 + $i, + 'url' => 'http://some.url/' . $i, + ], + ]; + } + $collection->batchInsert($rows); + } + + // Tests : + + public function testAll() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer')->all($connection); + $this->assertEquals(10, count($rows)); + } + + public function testDirectMatch() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where(['name' => 'name1']) + ->all($connection); + $this->assertEquals(1, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + } + + public function testIndexBy() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->indexBy('name') + ->all($connection); + $this->assertEquals(10, count($rows)); + $this->assertNotEmpty($rows['name1']); + } + + public function testInCondition() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where([ + 'name' => ['name1', 'name5'] + ]) + ->all($connection); + $this->assertEquals(2, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + $this->assertEquals('name5', $rows[1]['name']); + } + + public function testOrCondition() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where(['name' => 'name1']) + ->orWhere(['address' => 'address5']) + ->all($connection); + $this->assertEquals(2, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + $this->assertEquals('address5', $rows[1]['address']); + } + + public function testOrder() + { + $connection = $this->getConnection(); + + $query = new Query; + $rows = $query->from('customer') + ->orderBy(['name' => SORT_DESC]) + ->all($connection); + $this->assertEquals('name9', $rows[0]['name']); + + $query = new Query; + $rows = $query->from('customer') + ->orderBy(['avatar.height' => SORT_DESC]) + ->all($connection); + $this->assertEquals('name10', $rows[0]['name']); + } + + public function testMatchPlainId() + { + $connection = $this->getConnection(); + $query = new Query; + $row = $query->from('customer')->one($connection); + $query = new Query; + $rows = $query->from('customer') + ->where(['_id' => $row['_id']->__toString()]) + ->all($connection); + $this->assertEquals(1, count($rows)); + } + + public function testLike() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where(['LIKE', 'name', '/me1/']) + ->all($connection); + $this->assertEquals(2, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + $this->assertEquals('name10', $rows[1]['name']); + } } diff --git a/tests/unit/extensions/mongodb/QueryTest.php b/tests/unit/extensions/mongodb/QueryTest.php index eea57921a5b..d6f2da361f0 100644 --- a/tests/unit/extensions/mongodb/QueryTest.php +++ b/tests/unit/extensions/mongodb/QueryTest.php @@ -9,89 +9,89 @@ */ class QueryTest extends MongoDbTestCase { - public function testSelect() - { - // default - $query = new Query; - $select = []; - $query->select($select); - $this->assertEquals($select, $query->select); - - $query = new Query; - $select = ['name', 'something']; - $query->select($select); - $this->assertEquals($select, $query->select); - } - - public function testFrom() - { - $query = new Query; - $from = 'customer'; - $query->from($from); - $this->assertEquals($from, $query->from); - - $query = new Query; - $from = ['', 'customer']; - $query->from($from); - $this->assertEquals($from, $query->from); - } - - public function testWhere() - { - $query = new Query; - $query->where(['name' => 'name1']); - $this->assertEquals(['name' => 'name1'], $query->where); - - $query->andWhere(['address' => 'address1']); - $this->assertEquals( - [ - 'and', - ['name' => 'name1'], - ['address' => 'address1'] - ], - $query->where - ); - - $query->orWhere(['name' => 'name2']); - $this->assertEquals( - [ - 'or', - [ - 'and', - ['name' => 'name1'], - ['address' => 'address1'] - ], - ['name' => 'name2'] - - ], - $query->where - ); - } - - public function testOrder() - { - $query = new Query; - $query->orderBy('team'); - $this->assertEquals(['team' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('company'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('age'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); - - $query->addOrderBy(['age' => SORT_DESC]); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); - - $query->addOrderBy('age ASC, company DESC'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); - } - - public function testLimitOffset() - { - $query = new Query; - $query->limit(10)->offset(5); - $this->assertEquals(10, $query->limit); - $this->assertEquals(5, $query->offset); - } + public function testSelect() + { + // default + $query = new Query; + $select = []; + $query->select($select); + $this->assertEquals($select, $query->select); + + $query = new Query; + $select = ['name', 'something']; + $query->select($select); + $this->assertEquals($select, $query->select); + } + + public function testFrom() + { + $query = new Query; + $from = 'customer'; + $query->from($from); + $this->assertEquals($from, $query->from); + + $query = new Query; + $from = ['', 'customer']; + $query->from($from); + $this->assertEquals($from, $query->from); + } + + public function testWhere() + { + $query = new Query; + $query->where(['name' => 'name1']); + $this->assertEquals(['name' => 'name1'], $query->where); + + $query->andWhere(['address' => 'address1']); + $this->assertEquals( + [ + 'and', + ['name' => 'name1'], + ['address' => 'address1'] + ], + $query->where + ); + + $query->orWhere(['name' => 'name2']); + $this->assertEquals( + [ + 'or', + [ + 'and', + ['name' => 'name1'], + ['address' => 'address1'] + ], + ['name' => 'name2'] + + ], + $query->where + ); + } + + public function testOrder() + { + $query = new Query; + $query->orderBy('team'); + $this->assertEquals(['team' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('company'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('age'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); + + $query->addOrderBy(['age' => SORT_DESC]); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); + + $query->addOrderBy('age ASC, company DESC'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); + } + + public function testLimitOffset() + { + $query = new Query; + $query->limit(10)->offset(5); + $this->assertEquals(10, $query->limit); + $this->assertEquals(5, $query->offset); + } } diff --git a/tests/unit/extensions/mongodb/SessionTest.php b/tests/unit/extensions/mongodb/SessionTest.php index ccf9fda0b0b..ba47b0bda7e 100644 --- a/tests/unit/extensions/mongodb/SessionTest.php +++ b/tests/unit/extensions/mongodb/SessionTest.php @@ -7,134 +7,134 @@ class SessionTest extends MongoDbTestCase { - /** - * @var string test session collection name. - */ - protected static $sessionCollection = '_test_session'; - - protected function tearDown() - { - $this->dropCollection(static::$sessionCollection); - parent::tearDown(); - } - - /** - * Creates test session instance. - * @return Session session instance. - */ - protected function createSession() - { - return Yii::createObject([ - 'class' => Session::className(), - 'db' => $this->getConnection(), - 'sessionCollection' => static::$sessionCollection, - ]); - } - - // Tests: - - public function testWriteSession() - { - $session = $this->createSession(); - - $id = uniqid(); - $data = [ - 'name' => 'value' - ]; - $dataSerialized = serialize($data); - $this->assertTrue($session->writeSession($id, $dataSerialized), 'Unable to write session!'); - - $collection = $session->db->getCollection($session->sessionCollection); - $rows = $this->findAll($collection); - $this->assertCount(1, $rows, 'No session record!'); - - $row = array_shift($rows); - $this->assertEquals($id, $row['id'], 'Wrong session id!'); - $this->assertEquals($dataSerialized, $row['data'], 'Wrong session data!'); - $this->assertTrue($row['expire'] > time(), 'Wrong session expire!'); - - $newData = [ - 'name' => 'new value' - ]; - $newDataSerialized = serialize($newData); - $this->assertTrue($session->writeSession($id, $newDataSerialized), 'Unable to update session!'); - - $rows = $this->findAll($collection); - $this->assertCount(1, $rows, 'Wrong session records after update!'); - $newRow = array_shift($rows); - $this->assertEquals($id, $newRow['id'], 'Wrong session id after update!'); - $this->assertEquals($newDataSerialized, $newRow['data'], 'Wrong session data after update!'); - $this->assertTrue($newRow['expire'] >= $row['expire'], 'Wrong session expire after update!'); - } - - /** - * @depends testWriteSession - */ - public function testDestroySession() - { - $session = $this->createSession(); - - $id = uniqid(); - $data = [ - 'name' => 'value' - ]; - $dataSerialized = serialize($data); - $session->writeSession($id, $dataSerialized); - - $this->assertTrue($session->destroySession($id), 'Unable to destroy session!'); - - $collection = $session->db->getCollection($session->sessionCollection); - $rows = $this->findAll($collection); - $this->assertEmpty($rows, 'Session record not deleted!'); - } - - /** - * @depends testWriteSession - */ - public function testReadSession() - { - $session = $this->createSession(); - - $id = uniqid(); - $data = [ - 'name' => 'value' - ]; - $dataSerialized = serialize($data); - $session->writeSession($id, $dataSerialized); - - $sessionData = $session->readSession($id); - $this->assertEquals($dataSerialized, $sessionData, 'Unable to read session!'); - - $collection = $session->db->getCollection($session->sessionCollection); - list($row) = $this->findAll($collection); - $newRow = $row; - $newRow['expire'] = time() - 1; - unset($newRow['_id']); - $collection->update(['_id' => $row['_id']], $newRow); - - $sessionData = $session->readSession($id); - $this->assertEquals('', $sessionData, 'Expired session read!'); - } - - public function testGcSession() - { - $session = $this->createSession(); - $collection = $session->db->getCollection($session->sessionCollection); - $collection->batchInsert([ - [ - 'id' => uniqid(), - 'expire' => time() + 10, - 'data' => 'actual', - ], - [ - 'id' => uniqid(), - 'expire' => time() - 10, - 'data' => 'expired', - ], - ]); - $this->assertTrue($session->gcSession(10), 'Unable to collection garbage session!'); - - $rows = $this->findAll($collection); - $this->assertCount(1, $rows, 'Wrong records count!'); - } + /** + * @var string test session collection name. + */ + protected static $sessionCollection = '_test_session'; + + protected function tearDown() + { + $this->dropCollection(static::$sessionCollection); + parent::tearDown(); + } + + /** + * Creates test session instance. + * @return Session session instance. + */ + protected function createSession() + { + return Yii::createObject([ + 'class' => Session::className(), + 'db' => $this->getConnection(), + 'sessionCollection' => static::$sessionCollection, + ]); + } + + // Tests: + + public function testWriteSession() + { + $session = $this->createSession(); + + $id = uniqid(); + $data = [ + 'name' => 'value' + ]; + $dataSerialized = serialize($data); + $this->assertTrue($session->writeSession($id, $dataSerialized), 'Unable to write session!'); + + $collection = $session->db->getCollection($session->sessionCollection); + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'No session record!'); + + $row = array_shift($rows); + $this->assertEquals($id, $row['id'], 'Wrong session id!'); + $this->assertEquals($dataSerialized, $row['data'], 'Wrong session data!'); + $this->assertTrue($row['expire'] > time(), 'Wrong session expire!'); + + $newData = [ + 'name' => 'new value' + ]; + $newDataSerialized = serialize($newData); + $this->assertTrue($session->writeSession($id, $newDataSerialized), 'Unable to update session!'); + + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'Wrong session records after update!'); + $newRow = array_shift($rows); + $this->assertEquals($id, $newRow['id'], 'Wrong session id after update!'); + $this->assertEquals($newDataSerialized, $newRow['data'], 'Wrong session data after update!'); + $this->assertTrue($newRow['expire'] >= $row['expire'], 'Wrong session expire after update!'); + } + + /** + * @depends testWriteSession + */ + public function testDestroySession() + { + $session = $this->createSession(); + + $id = uniqid(); + $data = [ + 'name' => 'value' + ]; + $dataSerialized = serialize($data); + $session->writeSession($id, $dataSerialized); + + $this->assertTrue($session->destroySession($id), 'Unable to destroy session!'); + + $collection = $session->db->getCollection($session->sessionCollection); + $rows = $this->findAll($collection); + $this->assertEmpty($rows, 'Session record not deleted!'); + } + + /** + * @depends testWriteSession + */ + public function testReadSession() + { + $session = $this->createSession(); + + $id = uniqid(); + $data = [ + 'name' => 'value' + ]; + $dataSerialized = serialize($data); + $session->writeSession($id, $dataSerialized); + + $sessionData = $session->readSession($id); + $this->assertEquals($dataSerialized, $sessionData, 'Unable to read session!'); + + $collection = $session->db->getCollection($session->sessionCollection); + list($row) = $this->findAll($collection); + $newRow = $row; + $newRow['expire'] = time() - 1; + unset($newRow['_id']); + $collection->update(['_id' => $row['_id']], $newRow); + + $sessionData = $session->readSession($id); + $this->assertEquals('', $sessionData, 'Expired session read!'); + } + + public function testGcSession() + { + $session = $this->createSession(); + $collection = $session->db->getCollection($session->sessionCollection); + $collection->batchInsert([ + [ + 'id' => uniqid(), + 'expire' => time() + 10, + 'data' => 'actual', + ], + [ + 'id' => uniqid(), + 'expire' => time() - 10, + 'data' => 'expired', + ], + ]); + $this->assertTrue($session->gcSession(10), 'Unable to collection garbage session!'); + + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'Wrong records count!'); + } } diff --git a/tests/unit/extensions/mongodb/file/ActiveRecordTest.php b/tests/unit/extensions/mongodb/file/ActiveRecordTest.php index de4929401a5..6b0629c45af 100644 --- a/tests/unit/extensions/mongodb/file/ActiveRecordTest.php +++ b/tests/unit/extensions/mongodb/file/ActiveRecordTest.php @@ -14,311 +14,311 @@ */ class ActiveRecordTest extends MongoDbTestCase { - /** - * @var array[] list of test rows. - */ - protected $testRows = []; - - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - $this->setUpTestRows(); - $filePath = $this->getTestFilePath(); - if (!file_exists($filePath)) { - FileHelper::createDirectory($filePath); - } - } - - protected function tearDown() - { - $filePath = $this->getTestFilePath(); - if (file_exists($filePath)) { - FileHelper::removeDirectory($filePath); - } - $this->dropFileCollection(CustomerFile::collectionName()); - parent::tearDown(); - } - - /** - * @return string test file path. - */ - protected function getTestFilePath() - { - return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid(); - } - - /** - * Sets up test rows. - */ - protected function setUpTestRows() - { - $collection = $this->getConnection()->getFileCollection(CustomerFile::collectionName()); - $rows = []; - for ($i = 1; $i <= 10; $i++) { - $record = [ - 'tag' => 'tag' . $i, - 'status' => $i, - ]; - $content = 'content' . $i; - $record['_id'] = $collection->insertFileContent($content, $record); - $record['content'] = $content; - $rows[] = $record; - } - $this->testRows = $rows; - } - - // Tests : - - public function testFind() - { - // find one - $result = CustomerFile::find(); - $this->assertTrue($result instanceof ActiveQuery); - $customer = $result->one(); - $this->assertTrue($customer instanceof CustomerFile); - - // find all - $customers = CustomerFile::find()->all(); - $this->assertEquals(10, count($customers)); - $this->assertTrue($customers[0] instanceof CustomerFile); - $this->assertTrue($customers[1] instanceof CustomerFile); - - // find by _id - $testId = $this->testRows[0]['_id']; - $customer = CustomerFile::find($testId); - $this->assertTrue($customer instanceof CustomerFile); - $this->assertEquals($testId, $customer->_id); - - // find by column values - $customer = CustomerFile::find(['tag' => 'tag5']); - $this->assertTrue($customer instanceof CustomerFile); - $this->assertEquals($this->testRows[4]['_id'], $customer->_id); - $this->assertEquals('tag5', $customer->tag); - $customer = CustomerFile::find(['tag' => 'unexisting tag']); - $this->assertNull($customer); - - // find by attributes - $customer = CustomerFile::find()->where(['status' => 4])->one(); - $this->assertTrue($customer instanceof CustomerFile); - $this->assertEquals(4, $customer->status); - - // find count, sum, average, min, max, distinct - $this->assertEquals(10, CustomerFile::find()->count()); - $this->assertEquals(1, CustomerFile::find()->where(['status' => 2])->count()); - $this->assertEquals((1+10)/2*10, CustomerFile::find()->sum('status')); - $this->assertEquals((1+10)/2, CustomerFile::find()->average('status')); - $this->assertEquals(1, CustomerFile::find()->min('status')); - $this->assertEquals(10, CustomerFile::find()->max('status')); - $this->assertEquals(range(1, 10), CustomerFile::find()->distinct('status')); - - // scope - $this->assertEquals(1, CustomerFile::find()->activeOnly()->count()); - - // asArray - $testRow = $this->testRows[2]; - $customer = CustomerFile::find()->where(['_id' => $testRow['_id']])->asArray()->one(); - $this->assertEquals($testRow['_id'], $customer['_id']); - $this->assertEquals($testRow['tag'], $customer['tag']); - $this->assertEquals($testRow['status'], $customer['status']); - - // indexBy - $customers = CustomerFile::find()->indexBy('tag')->all(); - $this->assertTrue($customers['tag1'] instanceof CustomerFile); - $this->assertTrue($customers['tag2'] instanceof CustomerFile); - - // indexBy callable - $customers = CustomerFile::find()->indexBy(function ($customer) { - return $customer->status . '-' . $customer->status; - })->all(); - $this->assertTrue($customers['1-1'] instanceof CustomerFile); - $this->assertTrue($customers['2-2'] instanceof CustomerFile); - } - - public function testInsert() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - - $this->assertTrue($record->isNewRecord); - - $record->save(); - - $this->assertTrue($record->_id instanceof \MongoId); - $this->assertFalse($record->isNewRecord); - - $fileContent = $record->getFileContent(); - $this->assertEmpty($fileContent); - } - - /** - * @depends testInsert - */ - public function testInsertFile() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - - $fileName = __FILE__; - $record->setAttribute('file', $fileName); - - $record->save(); - - $this->assertTrue($record->_id instanceof \MongoId); - $this->assertFalse($record->isNewRecord); - - $fileContent = $record->getFileContent(); - $this->assertEquals(file_get_contents($fileName), $fileContent); - } - - /** - * @depends testInsert - */ - public function testInsertFileContent() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - - $newFileContent = 'Test new file content'; - $record->setAttribute('newFileContent', $newFileContent); - - $record->save(); - - $this->assertTrue($record->_id instanceof \MongoId); - $this->assertFalse($record->isNewRecord); - - $fileContent = $record->getFileContent(); - $this->assertEquals($newFileContent, $fileContent); - } - - /** - * @depends testInsert - */ - public function testUpdate() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - $record->save(); - - // save - $record = CustomerFile::find($record->_id); - $this->assertTrue($record instanceof CustomerFile); - $this->assertEquals(7, $record->status); - $this->assertFalse($record->isNewRecord); - - $record->status = 9; - $record->save(); - $this->assertEquals(9, $record->status); - $this->assertFalse($record->isNewRecord); - $record2 = CustomerFile::find($record->_id); - $this->assertEquals(9, $record2->status); - - // updateAll - $pk = ['_id' => $record->_id]; - $ret = CustomerFile::updateAll(['status' => 55], $pk); - $this->assertEquals(1, $ret); - $record = CustomerFile::find($pk); - $this->assertEquals(55, $record->status); - } - - /** - * @depends testUpdate - * @depends testInsertFileContent - */ - public function testUpdateFile() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - $newFileContent = 'Test new file content'; - $record->setAttribute('newFileContent', $newFileContent); - $record->save(); - - $updateFileName = __FILE__; - $record = CustomerFile::find($record->_id); - $record->setAttribute('file', $updateFileName); - $record->status = 55; - $record->save(); - $this->assertEquals(file_get_contents($updateFileName), $record->getFileContent()); - - $record2 = CustomerFile::find($record->_id); - $this->assertEquals($record->status, $record2->status); - $this->assertEquals(file_get_contents($updateFileName), $record2->getFileContent()); - $this->assertEquals($record->tag, $record2->tag); - } - - /** - * @depends testUpdate - * @depends testInsertFileContent - */ - public function testUpdateFileContent() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - $newFileContent = 'Test new file content'; - $record->setAttribute('newFileContent', $newFileContent); - $record->save(); - - $updateFileContent = 'New updated file content'; - $record = CustomerFile::find($record->_id); - $record->setAttribute('newFileContent', $updateFileContent); - $record->status = 55; - $record->save(); - $this->assertEquals($updateFileContent, $record->getFileContent()); - - $record2 = CustomerFile::find($record->_id); - $this->assertEquals($record->status, $record2->status); - $this->assertEquals($updateFileContent, $record2->getFileContent()); - } - - /** - * @depends testInsertFileContent - */ - public function testWriteFile() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - $newFileContent = 'Test new file content'; - $record->setAttribute('newFileContent', $newFileContent); - $record->save(); - - $outputFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . 'out.txt'; - $this->assertTrue($record->writeFile($outputFileName)); - $this->assertEquals($newFileContent, file_get_contents($outputFileName)); - - $record2 = CustomerFile::find($record->_id); - $outputFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . 'out_refreshed.txt'; - $this->assertTrue($record2->writeFile($outputFileName)); - $this->assertEquals($newFileContent, file_get_contents($outputFileName)); - } - - /** - * @depends testInsertFileContent - */ - public function testGetFileResource() - { - $record = new CustomerFile; - $record->tag = 'new new'; - $record->status = 7; - $newFileContent = 'Test new file content'; - $record->setAttribute('newFileContent', $newFileContent); - $record->save(); - - $fileResource = $record->getFileResource(); - $contents = stream_get_contents($fileResource); - fclose($fileResource); - $this->assertEquals($newFileContent, $contents); - - $record2 = CustomerFile::find($record->_id); - $fileResource = $record2->getFileResource(); - $contents = stream_get_contents($fileResource); - fclose($fileResource); - $this->assertEquals($newFileContent, $contents); - } + /** + * @var array[] list of test rows. + */ + protected $testRows = []; + + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + $this->setUpTestRows(); + $filePath = $this->getTestFilePath(); + if (!file_exists($filePath)) { + FileHelper::createDirectory($filePath); + } + } + + protected function tearDown() + { + $filePath = $this->getTestFilePath(); + if (file_exists($filePath)) { + FileHelper::removeDirectory($filePath); + } + $this->dropFileCollection(CustomerFile::collectionName()); + parent::tearDown(); + } + + /** + * @return string test file path. + */ + protected function getTestFilePath() + { + return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid(); + } + + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $collection = $this->getConnection()->getFileCollection(CustomerFile::collectionName()); + $rows = []; + for ($i = 1; $i <= 10; $i++) { + $record = [ + 'tag' => 'tag' . $i, + 'status' => $i, + ]; + $content = 'content' . $i; + $record['_id'] = $collection->insertFileContent($content, $record); + $record['content'] = $content; + $rows[] = $record; + } + $this->testRows = $rows; + } + + // Tests : + + public function testFind() + { + // find one + $result = CustomerFile::find(); + $this->assertTrue($result instanceof ActiveQuery); + $customer = $result->one(); + $this->assertTrue($customer instanceof CustomerFile); + + // find all + $customers = CustomerFile::find()->all(); + $this->assertEquals(10, count($customers)); + $this->assertTrue($customers[0] instanceof CustomerFile); + $this->assertTrue($customers[1] instanceof CustomerFile); + + // find by _id + $testId = $this->testRows[0]['_id']; + $customer = CustomerFile::find($testId); + $this->assertTrue($customer instanceof CustomerFile); + $this->assertEquals($testId, $customer->_id); + + // find by column values + $customer = CustomerFile::find(['tag' => 'tag5']); + $this->assertTrue($customer instanceof CustomerFile); + $this->assertEquals($this->testRows[4]['_id'], $customer->_id); + $this->assertEquals('tag5', $customer->tag); + $customer = CustomerFile::find(['tag' => 'unexisting tag']); + $this->assertNull($customer); + + // find by attributes + $customer = CustomerFile::find()->where(['status' => 4])->one(); + $this->assertTrue($customer instanceof CustomerFile); + $this->assertEquals(4, $customer->status); + + // find count, sum, average, min, max, distinct + $this->assertEquals(10, CustomerFile::find()->count()); + $this->assertEquals(1, CustomerFile::find()->where(['status' => 2])->count()); + $this->assertEquals((1+10)/2*10, CustomerFile::find()->sum('status')); + $this->assertEquals((1+10)/2, CustomerFile::find()->average('status')); + $this->assertEquals(1, CustomerFile::find()->min('status')); + $this->assertEquals(10, CustomerFile::find()->max('status')); + $this->assertEquals(range(1, 10), CustomerFile::find()->distinct('status')); + + // scope + $this->assertEquals(1, CustomerFile::find()->activeOnly()->count()); + + // asArray + $testRow = $this->testRows[2]; + $customer = CustomerFile::find()->where(['_id' => $testRow['_id']])->asArray()->one(); + $this->assertEquals($testRow['_id'], $customer['_id']); + $this->assertEquals($testRow['tag'], $customer['tag']); + $this->assertEquals($testRow['status'], $customer['status']); + + // indexBy + $customers = CustomerFile::find()->indexBy('tag')->all(); + $this->assertTrue($customers['tag1'] instanceof CustomerFile); + $this->assertTrue($customers['tag2'] instanceof CustomerFile); + + // indexBy callable + $customers = CustomerFile::find()->indexBy(function ($customer) { + return $customer->status . '-' . $customer->status; + })->all(); + $this->assertTrue($customers['1-1'] instanceof CustomerFile); + $this->assertTrue($customers['2-2'] instanceof CustomerFile); + } + + public function testInsert() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + + $this->assertTrue($record->isNewRecord); + + $record->save(); + + $this->assertTrue($record->_id instanceof \MongoId); + $this->assertFalse($record->isNewRecord); + + $fileContent = $record->getFileContent(); + $this->assertEmpty($fileContent); + } + + /** + * @depends testInsert + */ + public function testInsertFile() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + + $fileName = __FILE__; + $record->setAttribute('file', $fileName); + + $record->save(); + + $this->assertTrue($record->_id instanceof \MongoId); + $this->assertFalse($record->isNewRecord); + + $fileContent = $record->getFileContent(); + $this->assertEquals(file_get_contents($fileName), $fileContent); + } + + /** + * @depends testInsert + */ + public function testInsertFileContent() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + + $newFileContent = 'Test new file content'; + $record->setAttribute('newFileContent', $newFileContent); + + $record->save(); + + $this->assertTrue($record->_id instanceof \MongoId); + $this->assertFalse($record->isNewRecord); + + $fileContent = $record->getFileContent(); + $this->assertEquals($newFileContent, $fileContent); + } + + /** + * @depends testInsert + */ + public function testUpdate() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + $record->save(); + + // save + $record = CustomerFile::find($record->_id); + $this->assertTrue($record instanceof CustomerFile); + $this->assertEquals(7, $record->status); + $this->assertFalse($record->isNewRecord); + + $record->status = 9; + $record->save(); + $this->assertEquals(9, $record->status); + $this->assertFalse($record->isNewRecord); + $record2 = CustomerFile::find($record->_id); + $this->assertEquals(9, $record2->status); + + // updateAll + $pk = ['_id' => $record->_id]; + $ret = CustomerFile::updateAll(['status' => 55], $pk); + $this->assertEquals(1, $ret); + $record = CustomerFile::find($pk); + $this->assertEquals(55, $record->status); + } + + /** + * @depends testUpdate + * @depends testInsertFileContent + */ + public function testUpdateFile() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + $newFileContent = 'Test new file content'; + $record->setAttribute('newFileContent', $newFileContent); + $record->save(); + + $updateFileName = __FILE__; + $record = CustomerFile::find($record->_id); + $record->setAttribute('file', $updateFileName); + $record->status = 55; + $record->save(); + $this->assertEquals(file_get_contents($updateFileName), $record->getFileContent()); + + $record2 = CustomerFile::find($record->_id); + $this->assertEquals($record->status, $record2->status); + $this->assertEquals(file_get_contents($updateFileName), $record2->getFileContent()); + $this->assertEquals($record->tag, $record2->tag); + } + + /** + * @depends testUpdate + * @depends testInsertFileContent + */ + public function testUpdateFileContent() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + $newFileContent = 'Test new file content'; + $record->setAttribute('newFileContent', $newFileContent); + $record->save(); + + $updateFileContent = 'New updated file content'; + $record = CustomerFile::find($record->_id); + $record->setAttribute('newFileContent', $updateFileContent); + $record->status = 55; + $record->save(); + $this->assertEquals($updateFileContent, $record->getFileContent()); + + $record2 = CustomerFile::find($record->_id); + $this->assertEquals($record->status, $record2->status); + $this->assertEquals($updateFileContent, $record2->getFileContent()); + } + + /** + * @depends testInsertFileContent + */ + public function testWriteFile() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + $newFileContent = 'Test new file content'; + $record->setAttribute('newFileContent', $newFileContent); + $record->save(); + + $outputFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . 'out.txt'; + $this->assertTrue($record->writeFile($outputFileName)); + $this->assertEquals($newFileContent, file_get_contents($outputFileName)); + + $record2 = CustomerFile::find($record->_id); + $outputFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . 'out_refreshed.txt'; + $this->assertTrue($record2->writeFile($outputFileName)); + $this->assertEquals($newFileContent, file_get_contents($outputFileName)); + } + + /** + * @depends testInsertFileContent + */ + public function testGetFileResource() + { + $record = new CustomerFile; + $record->tag = 'new new'; + $record->status = 7; + $newFileContent = 'Test new file content'; + $record->setAttribute('newFileContent', $newFileContent); + $record->save(); + + $fileResource = $record->getFileResource(); + $contents = stream_get_contents($fileResource); + fclose($fileResource); + $this->assertEquals($newFileContent, $contents); + + $record2 = CustomerFile::find($record->_id); + $fileResource = $record2->getFileResource(); + $contents = stream_get_contents($fileResource); + fclose($fileResource); + $this->assertEquals($newFileContent, $contents); + } } diff --git a/tests/unit/extensions/mongodb/file/CollectionTest.php b/tests/unit/extensions/mongodb/file/CollectionTest.php index dec4c345bff..5e594c2a0d8 100644 --- a/tests/unit/extensions/mongodb/file/CollectionTest.php +++ b/tests/unit/extensions/mongodb/file/CollectionTest.php @@ -9,90 +9,90 @@ */ class CollectionTest extends MongoDbTestCase { - protected function tearDown() - { - $this->dropFileCollection('fs'); - parent::tearDown(); - } - - // Tests : - - public function testGetChunkCollection() - { - $collection = $this->getConnection()->getFileCollection(); - $chunkCollection = $collection->getChunkCollection(); - $this->assertTrue($chunkCollection instanceof \yii\mongodb\Collection); - $this->assertTrue($chunkCollection->mongoCollection instanceof \MongoCollection); - } - - public function testFind() - { - $collection = $this->getConnection()->getFileCollection(); - $cursor = $collection->find(); - $this->assertTrue($cursor instanceof \MongoGridFSCursor); - } - - public function testInsertFile() - { - $collection = $this->getConnection()->getFileCollection(); - - $filename = __FILE__; - $id = $collection->insertFile($filename); - $this->assertTrue($id instanceof \MongoId); - - $files = $this->findAll($collection); - $this->assertEquals(1, count($files)); - - /** @var $file \MongoGridFSFile */ - $file = $files[0]; - $this->assertEquals($filename, $file->getFilename()); - $this->assertEquals(file_get_contents($filename), $file->getBytes()); - } - - public function testInsertFileContent() - { - $collection = $this->getConnection()->getFileCollection(); - - $bytes = 'Test file content'; - $id = $collection->insertFileContent($bytes); - $this->assertTrue($id instanceof \MongoId); - - $files = $this->findAll($collection); - $this->assertEquals(1, count($files)); - - /** @var $file \MongoGridFSFile */ - $file = $files[0]; - $this->assertEquals($bytes, $file->getBytes()); - } - - /** - * @depends testInsertFileContent - */ - public function testGet() - { - $collection = $this->getConnection()->getFileCollection(); - - $bytes = 'Test file content'; - $id = $collection->insertFileContent($bytes); - - $file = $collection->get($id); - $this->assertTrue($file instanceof \MongoGridFSFile); - $this->assertEquals($bytes, $file->getBytes()); - } - - /** - * @depends testGet - */ - public function testDelete() - { - $collection = $this->getConnection()->getFileCollection(); - - $bytes = 'Test file content'; - $id = $collection->insertFileContent($bytes); - - $this->assertTrue($collection->delete($id)); - - $file = $collection->get($id); - $this->assertNull($file); - } + protected function tearDown() + { + $this->dropFileCollection('fs'); + parent::tearDown(); + } + + // Tests : + + public function testGetChunkCollection() + { + $collection = $this->getConnection()->getFileCollection(); + $chunkCollection = $collection->getChunkCollection(); + $this->assertTrue($chunkCollection instanceof \yii\mongodb\Collection); + $this->assertTrue($chunkCollection->mongoCollection instanceof \MongoCollection); + } + + public function testFind() + { + $collection = $this->getConnection()->getFileCollection(); + $cursor = $collection->find(); + $this->assertTrue($cursor instanceof \MongoGridFSCursor); + } + + public function testInsertFile() + { + $collection = $this->getConnection()->getFileCollection(); + + $filename = __FILE__; + $id = $collection->insertFile($filename); + $this->assertTrue($id instanceof \MongoId); + + $files = $this->findAll($collection); + $this->assertEquals(1, count($files)); + + /** @var $file \MongoGridFSFile */ + $file = $files[0]; + $this->assertEquals($filename, $file->getFilename()); + $this->assertEquals(file_get_contents($filename), $file->getBytes()); + } + + public function testInsertFileContent() + { + $collection = $this->getConnection()->getFileCollection(); + + $bytes = 'Test file content'; + $id = $collection->insertFileContent($bytes); + $this->assertTrue($id instanceof \MongoId); + + $files = $this->findAll($collection); + $this->assertEquals(1, count($files)); + + /** @var $file \MongoGridFSFile */ + $file = $files[0]; + $this->assertEquals($bytes, $file->getBytes()); + } + + /** + * @depends testInsertFileContent + */ + public function testGet() + { + $collection = $this->getConnection()->getFileCollection(); + + $bytes = 'Test file content'; + $id = $collection->insertFileContent($bytes); + + $file = $collection->get($id); + $this->assertTrue($file instanceof \MongoGridFSFile); + $this->assertEquals($bytes, $file->getBytes()); + } + + /** + * @depends testGet + */ + public function testDelete() + { + $collection = $this->getConnection()->getFileCollection(); + + $bytes = 'Test file content'; + $id = $collection->insertFileContent($bytes); + + $this->assertTrue($collection->delete($id)); + + $file = $collection->get($id); + $this->assertNull($file); + } } diff --git a/tests/unit/extensions/mongodb/file/QueryTest.php b/tests/unit/extensions/mongodb/file/QueryTest.php index 774c98f5da8..bcb2eeb32e5 100644 --- a/tests/unit/extensions/mongodb/file/QueryTest.php +++ b/tests/unit/extensions/mongodb/file/QueryTest.php @@ -10,61 +10,61 @@ */ class QueryTest extends MongoDbTestCase { - protected function setUp() - { - parent::setUp(); - $this->setUpTestRows(); - } + protected function setUp() + { + parent::setUp(); + $this->setUpTestRows(); + } - protected function tearDown() - { - $this->dropFileCollection(); - parent::tearDown(); - } + protected function tearDown() + { + $this->dropFileCollection(); + parent::tearDown(); + } - /** - * Sets up test rows. - */ - protected function setUpTestRows() - { - $collection = $this->getConnection()->getFileCollection(); - for ($i = 1; $i <= 10; $i++) { - $collection->insertFileContent('content' . $i, [ - 'filename' => 'name' . $i, - 'file_index' => $i, - ]); - } - } + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $collection = $this->getConnection()->getFileCollection(); + for ($i = 1; $i <= 10; $i++) { + $collection->insertFileContent('content' . $i, [ + 'filename' => 'name' . $i, + 'file_index' => $i, + ]); + } + } - // Tests : + // Tests : - public function testAll() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('fs')->all($connection); - $this->assertEquals(10, count($rows)); - } + public function testAll() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('fs')->all($connection); + $this->assertEquals(10, count($rows)); + } - public function testOne() - { - $connection = $this->getConnection(); - $query = new Query; - $row = $query->from('fs')->one($connection); - $this->assertTrue(is_array($row)); - $this->assertTrue($row['file'] instanceof \MongoGridFSFile); - } + public function testOne() + { + $connection = $this->getConnection(); + $query = new Query; + $row = $query->from('fs')->one($connection); + $this->assertTrue(is_array($row)); + $this->assertTrue($row['file'] instanceof \MongoGridFSFile); + } - public function testDirectMatch() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('fs') - ->where(['file_index' => 5]) - ->all($connection); - $this->assertEquals(1, count($rows)); - /** @var $file \MongoGridFSFile */ - $file = $rows[0]; - $this->assertEquals('name5', $file['filename']); - } + public function testDirectMatch() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('fs') + ->where(['file_index' => 5]) + ->all($connection); + $this->assertEquals(1, count($rows)); + /** @var $file \MongoGridFSFile */ + $file = $rows[0]; + $this->assertEquals('name5', $file['filename']); + } } diff --git a/tests/unit/extensions/redis/ActiveRecordTest.php b/tests/unit/extensions/redis/ActiveRecordTest.php index ce5bc11ddbb..810366d9f16 100644 --- a/tests/unit/extensions/redis/ActiveRecordTest.php +++ b/tests/unit/extensions/redis/ActiveRecordTest.php @@ -14,247 +14,246 @@ */ class ActiveRecordTest extends RedisTestCase { - use ActiveRecordTestTrait; - - public function callCustomerFind($q = null) - { - return Customer::find($q); - } - - public function callOrderFind($q = null) - { - return Order::find($q); - } - - public function callOrderItemFind($q = null) - { - return OrderItem::find($q); - } - - public function callItemFind($q = null) - { - return Item::find($q); - } - - public function getCustomerClass() - { - return Customer::className(); - } - - public function getItemClass() - { - return Item::className(); - } - - public function getOrderClass() - { - return Order::className(); - } - - public function getOrderItemClass() - { - return OrderItem::className(); - } - - - public function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - - $customer = new Customer(); - $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => null], false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2, 'profile_id' => 2], false); - $customer->save(false); + use ActiveRecordTestTrait; + + public function callCustomerFind($q = null) + { + return Customer::find($q); + } + + public function callOrderFind($q = null) + { + return Order::find($q); + } + + public function callOrderItemFind($q = null) + { + return OrderItem::find($q); + } + + public function callItemFind($q = null) + { + return Item::find($q); + } + + public function getCustomerClass() + { + return Customer::className(); + } + + public function getItemClass() + { + return Item::className(); + } + + public function getOrderClass() + { + return Order::className(); + } + + public function getOrderItemClass() + { + return OrderItem::className(); + } + + public function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2, 'profile_id' => 2], false); + $customer->save(false); // INSERT INTO tbl_category (name) VALUES ('Books'); // INSERT INTO tbl_category (name) VALUES ('Movies'); - $item = new Item(); - $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); - $item->save(false); - - $order = new Order(); - $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); - $order->save(false); - $order = new Order(); - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); - $order->save(false); - $order = new Order(); - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); - $order->save(false); - - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); - $orderItem->save(false); - } - - public function testFindNullValues() - { - // https://github.com/yiisoft/yii2/issues/1311 - $this->markTestSkipped('Redis does not store/find null values correctly.'); - } - - public function testBooleanAttribute() - { - // https://github.com/yiisoft/yii2/issues/1311 - $this->markTestSkipped('Redis does not store/find boolean values correctly.'); - } - - public function testFindEagerViaRelationPreserveOrder() - { - $this->markTestSkipped('Redis does not support orderBy.'); - } - - public function testFindEagerViaRelationPreserveOrderB() - { - $this->markTestSkipped('Redis does not support orderBy.'); - } - - public function testSatisticalFind() - { - // find count, sum, average, min, max, scalar - $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(6, Customer::find()->sum('id')); - $this->assertEquals(2, Customer::find()->average('id')); - $this->assertEquals(1, Customer::find()->min('id')); - $this->assertEquals(3, Customer::find()->max('id')); - - $this->assertEquals(6, OrderItem::find()->count()); - $this->assertEquals(7, OrderItem::find()->sum('quantity')); - } - - public function testfindIndexBy() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // indexBy - $customers = $this->callCustomerFind()->indexBy('name')/*->orderBy('id')*/->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['user1'] instanceof $customerClass); - $this->assertTrue($customers['user2'] instanceof $customerClass); - $this->assertTrue($customers['user3'] instanceof $customerClass); - - // indexBy callable - $customers = $this->callCustomerFind()->indexBy(function ($customer) { - return $customer->id . '-' . $customer->name; - })/*->orderBy('id')*/->all(); // TODO this test is duplicated because of missing orderBy support in redis - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['1-user1'] instanceof $customerClass); - $this->assertTrue($customers['2-user2'] instanceof $customerClass); - $this->assertTrue($customers['3-user3'] instanceof $customerClass); - } - - public function testFindLimit() - { - // TODO this test is duplicated because of missing orderBy support in redis - /** @var TestCase|ActiveRecordTestTrait $this */ - // all() - $customers = $this->callCustomerFind()->all(); - $this->assertEquals(3, count($customers)); - - $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user1', $customers[0]->name); - - $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->offset(1)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user2', $customers[0]->name); - - $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->offset(2)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user3', $customers[0]->name); - - $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(2)->offset(1)->all(); - $this->assertEquals(2, count($customers)); - $this->assertEquals('user2', $customers[0]->name); - $this->assertEquals('user3', $customers[1]->name); - - $customers = $this->callCustomerFind()->limit(2)->offset(3)->all(); - $this->assertEquals(0, count($customers)); - - // one() - $customer = $this->callCustomerFind()/*->orderBy('id')*/->one(); - $this->assertEquals('user1', $customer->name); - - $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(0)->one(); - $this->assertEquals('user1', $customer->name); - - $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(1)->one(); - $this->assertEquals('user2', $customer->name); - - $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(2)->one(); - $this->assertEquals('user3', $customer->name); - - $customer = $this->callCustomerFind()->offset(3)->one(); - $this->assertNull($customer); - } - - public function testFindEagerViaRelation() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $orders = $this->callOrderFind()->with('items')/*->orderBy('id')*/->all(); // TODO this test is duplicated because of missing orderBy support in redis - $this->assertEquals(3, count($orders)); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testFindColumn() - { - $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name')); - // TODO $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->column('name')); - } - - // TODO test serial column incr - - public function testUpdatePk() - { - // updateCounters - $pk = ['order_id' => 2, 'item_id' => 4]; - $orderItem = OrderItem::find($pk); - $this->assertEquals(2, $orderItem->order_id); - $this->assertEquals(4, $orderItem->item_id); - - $orderItem->order_id = 2; - $orderItem->item_id = 10; - $orderItem->save(); - - $this->assertNull(OrderItem::find($pk)); - $this->assertNotNull(OrderItem::find(['order_id' => 2, 'item_id' => 10])); - } + $item = new Item(); + $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); + $item->save(false); + + $order = new Order(); + $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + } + + public function testFindNullValues() + { + // https://github.com/yiisoft/yii2/issues/1311 + $this->markTestSkipped('Redis does not store/find null values correctly.'); + } + + public function testBooleanAttribute() + { + // https://github.com/yiisoft/yii2/issues/1311 + $this->markTestSkipped('Redis does not store/find boolean values correctly.'); + } + + public function testFindEagerViaRelationPreserveOrder() + { + $this->markTestSkipped('Redis does not support orderBy.'); + } + + public function testFindEagerViaRelationPreserveOrderB() + { + $this->markTestSkipped('Redis does not support orderBy.'); + } + + public function testSatisticalFind() + { + // find count, sum, average, min, max, scalar + $this->assertEquals(3, Customer::find()->count()); + $this->assertEquals(6, Customer::find()->sum('id')); + $this->assertEquals(2, Customer::find()->average('id')); + $this->assertEquals(1, Customer::find()->min('id')); + $this->assertEquals(3, Customer::find()->max('id')); + + $this->assertEquals(6, OrderItem::find()->count()); + $this->assertEquals(7, OrderItem::find()->sum('quantity')); + } + + public function testfindIndexBy() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // indexBy + $customers = $this->callCustomerFind()->indexBy('name')/*->orderBy('id')*/->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['user1'] instanceof $customerClass); + $this->assertTrue($customers['user2'] instanceof $customerClass); + $this->assertTrue($customers['user3'] instanceof $customerClass); + + // indexBy callable + $customers = $this->callCustomerFind()->indexBy(function ($customer) { + return $customer->id . '-' . $customer->name; + })/*->orderBy('id')*/->all(); // TODO this test is duplicated because of missing orderBy support in redis + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['1-user1'] instanceof $customerClass); + $this->assertTrue($customers['2-user2'] instanceof $customerClass); + $this->assertTrue($customers['3-user3'] instanceof $customerClass); + } + + public function testFindLimit() + { + // TODO this test is duplicated because of missing orderBy support in redis + /** @var TestCase|ActiveRecordTestTrait $this */ + // all() + $customers = $this->callCustomerFind()->all(); + $this->assertEquals(3, count($customers)); + + $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user1', $customers[0]->name); + + $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->offset(1)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user2', $customers[0]->name); + + $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->offset(2)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user3', $customers[0]->name); + + $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(2)->offset(1)->all(); + $this->assertEquals(2, count($customers)); + $this->assertEquals('user2', $customers[0]->name); + $this->assertEquals('user3', $customers[1]->name); + + $customers = $this->callCustomerFind()->limit(2)->offset(3)->all(); + $this->assertEquals(0, count($customers)); + + // one() + $customer = $this->callCustomerFind()/*->orderBy('id')*/->one(); + $this->assertEquals('user1', $customer->name); + + $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(0)->one(); + $this->assertEquals('user1', $customer->name); + + $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(1)->one(); + $this->assertEquals('user2', $customer->name); + + $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(2)->one(); + $this->assertEquals('user3', $customer->name); + + $customer = $this->callCustomerFind()->offset(3)->one(); + $this->assertNull($customer); + } + + public function testFindEagerViaRelation() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $orders = $this->callOrderFind()->with('items')/*->orderBy('id')*/->all(); // TODO this test is duplicated because of missing orderBy support in redis + $this->assertEquals(3, count($orders)); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testFindColumn() + { + $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name')); + // TODO $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->column('name')); + } + + // TODO test serial column incr + + public function testUpdatePk() + { + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + $orderItem = OrderItem::find($pk); + $this->assertEquals(2, $orderItem->order_id); + $this->assertEquals(4, $orderItem->item_id); + + $orderItem->order_id = 2; + $orderItem->item_id = 10; + $orderItem->save(); + + $this->assertNull(OrderItem::find($pk)); + $this->assertNotNull(OrderItem::find(['order_id' => 2, 'item_id' => 10])); + } } diff --git a/tests/unit/extensions/redis/RedisCacheTest.php b/tests/unit/extensions/redis/RedisCacheTest.php index 3e4e0bf4a18..6caab91b6b7 100644 --- a/tests/unit/extensions/redis/RedisCacheTest.php +++ b/tests/unit/extensions/redis/RedisCacheTest.php @@ -15,86 +15,87 @@ */ class RedisCacheTest extends CacheTestCase { - private $_cacheInstance = null; - - /** - * @return Cache - */ - protected function getCacheInstance() - { - $databases = $this->getParam('databases'); - $params = isset($databases['redis']) ? $databases['redis'] : null; - if ($params === null) { - $this->markTestSkipped('No redis server connection configured.'); - } - $connection = new Connection($params); - if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { - $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); - } - - $this->mockApplication(['components' => ['redis' => $connection]]); - - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new Cache(); - } - return $this->_cacheInstance; - } - - public function testExpireMilliseconds() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); - usleep(100000); - $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); - usleep(300000); - $this->assertFalse($cache->get('expire_test_ms')); - } - - public function testExpireAddMilliseconds() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2)); - usleep(100000); - $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms')); - usleep(300000); - $this->assertFalse($cache->get('expire_testa_ms')); - } - - /** - * Store a value that is 2 times buffer size big - * https://github.com/yiisoft/yii2/issues/743 - */ - public function testLargeData() - { - $cache = $this->getCacheInstance(); - - $data = str_repeat('XX', 8192); // http://www.php.net/manual/en/function.fread.php - $key = 'bigdata1'; - - $this->assertFalse($cache->get($key)); - $cache->set($key, $data); - $this->assertTrue($cache->get($key) === $data); - - // try with multibyte string - $data = str_repeat('ЖЫ', 8192); // http://www.php.net/manual/en/function.fread.php - $key = 'bigdata2'; - - $this->assertFalse($cache->get($key)); - $cache->set($key, $data); - $this->assertTrue($cache->get($key) === $data); - } - - public function testMultiByteGetAndSet() - { - $cache = $this->getCacheInstance(); - - $data = ['abc' => 'ежик', 2 => 'def']; - $key = 'data1'; - - $this->assertFalse($cache->get($key)); - $cache->set($key, $data); - $this->assertTrue($cache->get($key) === $data); - } + private $_cacheInstance = null; + + /** + * @return Cache + */ + protected function getCacheInstance() + { + $databases = $this->getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + if ($params === null) { + $this->markTestSkipped('No redis server connection configured.'); + } + $connection = new Connection($params); + if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + $this->mockApplication(['components' => ['redis' => $connection]]); + + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new Cache(); + } + + return $this->_cacheInstance; + } + + public function testExpireMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_test_ms')); + } + + public function testExpireAddMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_testa_ms')); + } + + /** + * Store a value that is 2 times buffer size big + * https://github.com/yiisoft/yii2/issues/743 + */ + public function testLargeData() + { + $cache = $this->getCacheInstance(); + + $data = str_repeat('XX', 8192); // http://www.php.net/manual/en/function.fread.php + $key = 'bigdata1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertTrue($cache->get($key) === $data); + + // try with multibyte string + $data = str_repeat('ЖЫ', 8192); // http://www.php.net/manual/en/function.fread.php + $key = 'bigdata2'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertTrue($cache->get($key) === $data); + } + + public function testMultiByteGetAndSet() + { + $cache = $this->getCacheInstance(); + + $data = ['abc' => 'ежик', 2 => 'def']; + $key = 'data1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertTrue($cache->get($key) === $data); + } } diff --git a/tests/unit/extensions/redis/RedisConnectionTest.php b/tests/unit/extensions/redis/RedisConnectionTest.php index fbc94e3fe18..5a6e20cb437 100644 --- a/tests/unit/extensions/redis/RedisConnectionTest.php +++ b/tests/unit/extensions/redis/RedisConnectionTest.php @@ -7,50 +7,50 @@ */ class RedisConnectionTest extends RedisTestCase { - /** - * test connection to redis and selection of db - */ - public function testConnect() - { - $db = $this->getConnection(false); - $db->open(); - $this->assertTrue($db->ping()); - $db->set('YIITESTKEY', 'YIITESTVALUE'); - $db->close(); + /** + * test connection to redis and selection of db + */ + public function testConnect() + { + $db = $this->getConnection(false); + $db->open(); + $this->assertTrue($db->ping()); + $db->set('YIITESTKEY', 'YIITESTVALUE'); + $db->close(); - $db = $this->getConnection(false); - $db->database = 0; - $db->open(); - $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); - $db->close(); + $db = $this->getConnection(false); + $db->database = 0; + $db->open(); + $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); + $db->close(); - $db = $this->getConnection(false); - $db->database = 1; - $db->open(); - $this->assertNull($db->get('YIITESTKEY')); - $db->close(); - } + $db = $this->getConnection(false); + $db->database = 1; + $db->open(); + $this->assertNull($db->get('YIITESTKEY')); + $db->close(); + } - public function keyValueData() - { - return [ - [123], - [-123], - [0], - ['test'], - ["test\r\ntest"], - [''], - ]; - } + public function keyValueData() + { + return [ + [123], + [-123], + [0], + ['test'], + ["test\r\ntest"], + [''], + ]; + } - /** - * @dataProvider keyValueData - */ - public function testStoreGet($data) - { - $db = $this->getConnection(true); + /** + * @dataProvider keyValueData + */ + public function testStoreGet($data) + { + $db = $this->getConnection(true); - $db->set('hi', $data); - $this->assertEquals($data, $db->get('hi')); - } + $db->set('hi', $data); + $this->assertEquals($data, $db->get('hi')); + } } diff --git a/tests/unit/extensions/redis/RedisTestCase.php b/tests/unit/extensions/redis/RedisTestCase.php index 9857e8df213..5a2d30cc3e5 100644 --- a/tests/unit/extensions/redis/RedisTestCase.php +++ b/tests/unit/extensions/redis/RedisTestCase.php @@ -13,36 +13,37 @@ */ abstract class RedisTestCase extends TestCase { - protected function setUp() - { - $databases = $this->getParam('databases'); - $params = isset($databases['redis']) ? $databases['redis'] : null; - if ($params === null) { - $this->markTestSkipped('No redis server connection configured.'); - } - $connection = new Connection($params); - if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { - $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); - } - - $this->mockApplication(['components' => ['redis' => $connection]]); - - parent::setUp(); - } - - /** - * @param boolean $reset whether to clean up the test database - * @return Connection - */ - public function getConnection($reset = true) - { - $databases = $this->getParam('databases'); - $params = isset($databases['redis']) ? $databases['redis'] : []; - $db = new Connection($params); - if ($reset) { - $db->open(); - $db->flushdb(); - } - return $db; - } + protected function setUp() + { + $databases = $this->getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + if ($params === null) { + $this->markTestSkipped('No redis server connection configured.'); + } + $connection = new Connection($params); + if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + $this->mockApplication(['components' => ['redis' => $connection]]); + + parent::setUp(); + } + + /** + * @param boolean $reset whether to clean up the test database + * @return Connection + */ + public function getConnection($reset = true) + { + $databases = $this->getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : []; + $db = new Connection($params); + if ($reset) { + $db->open(); + $db->flushdb(); + } + + return $db; + } } diff --git a/tests/unit/extensions/smarty/ViewRendererTest.php b/tests/unit/extensions/smarty/ViewRendererTest.php index bdb94919c3a..3b73ab9d177 100644 --- a/tests/unit/extensions/smarty/ViewRendererTest.php +++ b/tests/unit/extensions/smarty/ViewRendererTest.php @@ -1,7 +1,7 @@ */ @@ -17,54 +17,55 @@ */ class ViewRendererTest extends TestCase { - protected function setUp() - { - $this->mockApplication(); - } + protected function setUp() + { + $this->mockApplication(); + } - /** - * https://github.com/yiisoft/yii2/issues/2265 - */ - public function testNoParams() - { - $view = $this->mockView(); - $content = $view->renderFile('@yiiunit/extensions/smarty/views/simple.tpl'); + /** + * https://github.com/yiisoft/yii2/issues/2265 + */ + public function testNoParams() + { + $view = $this->mockView(); + $content = $view->renderFile('@yiiunit/extensions/smarty/views/simple.tpl'); - $this->assertEquals('simple view without parameters.', $content); - } + $this->assertEquals('simple view without parameters.', $content); + } - public function testRender() - { - $view = $this->mockView(); - $content = $view->renderFile('@yiiunit/extensions/smarty/views/view.tpl', ['param' => 'Hello World!']); + public function testRender() + { + $view = $this->mockView(); + $content = $view->renderFile('@yiiunit/extensions/smarty/views/view.tpl', ['param' => 'Hello World!']); - $this->assertEquals('test view Hello World!.', $content); - } + $this->assertEquals('test view Hello World!.', $content); + } - /** - * @return View - */ - protected function mockView() - { - return new View([ - 'renderers' => [ - 'tpl' => [ - 'class' => 'yii\smarty\ViewRenderer', - ], - ], - 'assetManager' => $this->mockAssetManager(), - ]); - } + /** + * @return View + */ + protected function mockView() + { + return new View([ + 'renderers' => [ + 'tpl' => [ + 'class' => 'yii\smarty\ViewRenderer', + ], + ], + 'assetManager' => $this->mockAssetManager(), + ]); + } - protected function mockAssetManager() - { - $assetDir = Yii::getAlias('@runtime/assets'); - if (!is_dir($assetDir)) { - mkdir($assetDir, 0777, true); - } - return new AssetManager([ - 'basePath' => $assetDir, - 'baseUrl' => '/assets', - ]); - } + protected function mockAssetManager() + { + $assetDir = Yii::getAlias('@runtime/assets'); + if (!is_dir($assetDir)) { + mkdir($assetDir, 0777, true); + } + + return new AssetManager([ + 'basePath' => $assetDir, + 'baseUrl' => '/assets', + ]); + } } diff --git a/tests/unit/extensions/sphinx/ActiveDataProviderTest.php b/tests/unit/extensions/sphinx/ActiveDataProviderTest.php index 3fa82022cc7..50841e5ffa1 100644 --- a/tests/unit/extensions/sphinx/ActiveDataProviderTest.php +++ b/tests/unit/extensions/sphinx/ActiveDataProviderTest.php @@ -12,55 +12,55 @@ */ class ActiveDataProviderTest extends SphinxTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } - // Tests : + // Tests : - public function testQuery() - { - $query = new Query; - $query->from('yii2_test_article_index'); + public function testQuery() + { + $query = new Query; + $query->from('yii2_test_article_index'); - $provider = new ActiveDataProvider([ - 'query' => $query, - 'db' => $this->getConnection(), - ]); - $models = $provider->getModels(); - $this->assertEquals(2, count($models)); + $provider = new ActiveDataProvider([ + 'query' => $query, + 'db' => $this->getConnection(), + ]); + $models = $provider->getModels(); + $this->assertEquals(2, count($models)); - $provider = new ActiveDataProvider([ - 'query' => $query, - 'db' => $this->getConnection(), - 'pagination' => [ - 'pageSize' => 1, - ] - ]); - $models = $provider->getModels(); - $this->assertEquals(1, count($models)); - } + $provider = new ActiveDataProvider([ + 'query' => $query, + 'db' => $this->getConnection(), + 'pagination' => [ + 'pageSize' => 1, + ] + ]); + $models = $provider->getModels(); + $this->assertEquals(1, count($models)); + } - public function testActiveQuery() - { - $provider = new ActiveDataProvider([ - 'query' => ArticleIndex::find()->orderBy('id ASC'), - ]); - $models = $provider->getModels(); - $this->assertEquals(2, count($models)); - $this->assertTrue($models[0] instanceof ArticleIndex); - $this->assertTrue($models[1] instanceof ArticleIndex); - $this->assertEquals([1, 2], $provider->getKeys()); + public function testActiveQuery() + { + $provider = new ActiveDataProvider([ + 'query' => ArticleIndex::find()->orderBy('id ASC'), + ]); + $models = $provider->getModels(); + $this->assertEquals(2, count($models)); + $this->assertTrue($models[0] instanceof ArticleIndex); + $this->assertTrue($models[1] instanceof ArticleIndex); + $this->assertEquals([1, 2], $provider->getKeys()); - $provider = new ActiveDataProvider([ - 'query' => ArticleIndex::find(), - 'pagination' => [ - 'pageSize' => 1, - ] - ]); - $models = $provider->getModels(); - $this->assertEquals(1, count($models)); - } + $provider = new ActiveDataProvider([ + 'query' => ArticleIndex::find(), + 'pagination' => [ + 'pageSize' => 1, + ] + ]); + $models = $provider->getModels(); + $this->assertEquals(1, count($models)); + } } diff --git a/tests/unit/extensions/sphinx/ActiveRecordTest.php b/tests/unit/extensions/sphinx/ActiveRecordTest.php index 5ebd9748192..cbf151943c3 100644 --- a/tests/unit/extensions/sphinx/ActiveRecordTest.php +++ b/tests/unit/extensions/sphinx/ActiveRecordTest.php @@ -12,226 +12,226 @@ */ class ActiveRecordTest extends SphinxTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - } - - protected function tearDown() - { - $this->truncateRuntimeIndex('yii2_test_rt_index'); - parent::tearDown(); - } - - // Tests : - - public function testFind() - { - // find one - $result = ArticleIndex::find(); - $this->assertTrue($result instanceof ActiveQuery); - $article = $result->one(); - $this->assertTrue($article instanceof ArticleIndex); - - // find all - $articles = ArticleIndex::find()->all(); - $this->assertEquals(2, count($articles)); - $this->assertTrue($articles[0] instanceof ArticleIndex); - $this->assertTrue($articles[1] instanceof ArticleIndex); - - // find fulltext - $article = ArticleIndex::find(2); - $this->assertTrue($article instanceof ArticleIndex); - $this->assertEquals(2, $article->id); - - // find by column values - $article = ArticleIndex::find(['id' => 2, 'author_id' => 2]); - $this->assertTrue($article instanceof ArticleIndex); - $this->assertEquals(2, $article->id); - $this->assertEquals(2, $article->author_id); - $article = ArticleIndex::find(['id' => 2, 'author_id' => 1]); - $this->assertNull($article); - - // find by attributes - $article = ArticleIndex::find()->where(['author_id' => 2])->one(); - $this->assertTrue($article instanceof ArticleIndex); - $this->assertEquals(2, $article->id); - - // find custom column - $article = ArticleIndex::find()->select(['*', '(5*2) AS custom_column']) - ->where(['author_id' => 1])->one(); - $this->assertEquals(1, $article->id); - $this->assertEquals(10, $article->custom_column); - - // find count, sum, average, min, max, scalar - $this->assertEquals(2, ArticleIndex::find()->count()); - $this->assertEquals(1, ArticleIndex::find()->where('id=1')->count()); - $this->assertEquals(3, ArticleIndex::find()->sum('id')); - $this->assertEquals(1.5, ArticleIndex::find()->average('id')); - $this->assertEquals(1, ArticleIndex::find()->min('id')); - $this->assertEquals(2, ArticleIndex::find()->max('id')); - $this->assertEquals(2, ArticleIndex::find()->select('COUNT(*)')->scalar()); - - // scope - $this->assertEquals(1, ArticleIndex::find()->favoriteAuthor()->count()); - - // asArray - $article = ArticleIndex::find()->where('id=2')->asArray()->one(); - unset($article['add_date']); - $this->assertEquals([ - 'id' => '2', - 'author_id' => '2', - 'tag' => '3,4', - ], $article); - - // indexBy - $articles = ArticleIndex::find()->indexBy('author_id')->orderBy('id DESC')->all(); - $this->assertEquals(2, count($articles)); - $this->assertTrue($articles['1'] instanceof ArticleIndex); - $this->assertTrue($articles['2'] instanceof ArticleIndex); - - // indexBy callable - $articles = ArticleIndex::find()->indexBy(function ($article) { - return $article->id . '-' . $article->author_id; - })->orderBy('id DESC')->all(); - $this->assertEquals(2, count($articles)); - $this->assertTrue($articles['1-1'] instanceof ArticleIndex); - $this->assertTrue($articles['2-2'] instanceof ArticleIndex); - } - - public function testFindBySql() - { - // find one - $article = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index ORDER BY id DESC')->one(); - $this->assertTrue($article instanceof ArticleIndex); - $this->assertEquals(2, $article->author_id); - - // find all - $articles = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index')->all(); - $this->assertEquals(2, count($articles)); - - // find with parameter binding - $article = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index WHERE id=:id', [':id' => 2])->one(); - $this->assertTrue($article instanceof ArticleIndex); - $this->assertEquals(2, $article->author_id); - } - - public function testInsert() - { - $record = new RuntimeIndex; - $record->id = 15; - $record->title = 'test title'; - $record->content = 'test content'; - $record->type_id = 7; - $record->category = [1, 2]; - - $this->assertTrue($record->isNewRecord); - - $record->save(); - - $this->assertEquals(15, $record->id); - $this->assertFalse($record->isNewRecord); - } - - /** - * @depends testInsert - */ - public function testUpdate() - { - $record = new RuntimeIndex; - $record->id = 2; - $record->title = 'test title'; - $record->content = 'test content'; - $record->type_id = 7; - $record->category = [1, 2]; - $record->save(); - - // save - $record = RuntimeIndex::find(2); - $this->assertTrue($record instanceof RuntimeIndex); - $this->assertEquals(7, $record->type_id); - $this->assertFalse($record->isNewRecord); - - $record->type_id = 9; - $record->save(); - $this->assertEquals(9, $record->type_id); - $this->assertFalse($record->isNewRecord); - $record2 = RuntimeIndex::find(['id' => 2]); - $this->assertEquals(9, $record2->type_id); - - // replace - $query = 'replace'; - $rows = RuntimeIndex::find()->match($query)->all(); - $this->assertEmpty($rows); - $record = RuntimeIndex::find(2); - $record->content = 'Test content with ' . $query; - $record->save(); - $rows = RuntimeIndex::find()->match($query); - $this->assertNotEmpty($rows); - - // updateAll - $pk = ['id' => 2]; - $ret = RuntimeIndex::updateAll(['type_id' => 55], $pk); - $this->assertEquals(1, $ret); - $record = RuntimeIndex::find($pk); - $this->assertEquals(55, $record->type_id); - } - - /** - * @depends testInsert - */ - public function testDelete() - { - // delete - $record = new RuntimeIndex; - $record->id = 2; - $record->title = 'test title'; - $record->content = 'test content'; - $record->type_id = 7; - $record->category = [1, 2]; - $record->save(); - - $record = RuntimeIndex::find(2); - $record->delete(); - $record = RuntimeIndex::find(2); - $this->assertNull($record); - - // deleteAll - $record = new RuntimeIndex; - $record->id = 2; - $record->title = 'test title'; - $record->content = 'test content'; - $record->type_id = 7; - $record->category = [1, 2]; - $record->save(); - - $ret = RuntimeIndex::deleteAll('id = 2'); - $this->assertEquals(1, $ret); - $records = RuntimeIndex::find()->all(); - $this->assertEquals(0, count($records)); - } - - public function testCallSnippets() - { - $query = 'pencil'; - $source = 'Some data sentence about ' . $query; - - $snippet = ArticleIndex::callSnippets($source, $query); - $this->assertNotEmpty($snippet, 'Unable to call snippets!'); - $this->assertContains('' . $query . '', $snippet, 'Query not present in the snippet!'); - - $rows = ArticleIndex::callSnippets([$source], $query); - $this->assertNotEmpty($rows, 'Unable to call snippets!'); - $this->assertContains('' . $query . '', $rows[0], 'Query not present in the snippet!'); - } - - public function testCallKeywords() - { - $text = 'table pencil'; - $rows = ArticleIndex::callKeywords($text); - $this->assertNotEmpty($rows, 'Unable to call keywords!'); - $this->assertArrayHasKey('tokenized', $rows[0], 'No tokenized keyword!'); - $this->assertArrayHasKey('normalized', $rows[0], 'No normalized keyword!'); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } + + protected function tearDown() + { + $this->truncateRuntimeIndex('yii2_test_rt_index'); + parent::tearDown(); + } + + // Tests : + + public function testFind() + { + // find one + $result = ArticleIndex::find(); + $this->assertTrue($result instanceof ActiveQuery); + $article = $result->one(); + $this->assertTrue($article instanceof ArticleIndex); + + // find all + $articles = ArticleIndex::find()->all(); + $this->assertEquals(2, count($articles)); + $this->assertTrue($articles[0] instanceof ArticleIndex); + $this->assertTrue($articles[1] instanceof ArticleIndex); + + // find fulltext + $article = ArticleIndex::find(2); + $this->assertTrue($article instanceof ArticleIndex); + $this->assertEquals(2, $article->id); + + // find by column values + $article = ArticleIndex::find(['id' => 2, 'author_id' => 2]); + $this->assertTrue($article instanceof ArticleIndex); + $this->assertEquals(2, $article->id); + $this->assertEquals(2, $article->author_id); + $article = ArticleIndex::find(['id' => 2, 'author_id' => 1]); + $this->assertNull($article); + + // find by attributes + $article = ArticleIndex::find()->where(['author_id' => 2])->one(); + $this->assertTrue($article instanceof ArticleIndex); + $this->assertEquals(2, $article->id); + + // find custom column + $article = ArticleIndex::find()->select(['*', '(5*2) AS custom_column']) + ->where(['author_id' => 1])->one(); + $this->assertEquals(1, $article->id); + $this->assertEquals(10, $article->custom_column); + + // find count, sum, average, min, max, scalar + $this->assertEquals(2, ArticleIndex::find()->count()); + $this->assertEquals(1, ArticleIndex::find()->where('id=1')->count()); + $this->assertEquals(3, ArticleIndex::find()->sum('id')); + $this->assertEquals(1.5, ArticleIndex::find()->average('id')); + $this->assertEquals(1, ArticleIndex::find()->min('id')); + $this->assertEquals(2, ArticleIndex::find()->max('id')); + $this->assertEquals(2, ArticleIndex::find()->select('COUNT(*)')->scalar()); + + // scope + $this->assertEquals(1, ArticleIndex::find()->favoriteAuthor()->count()); + + // asArray + $article = ArticleIndex::find()->where('id=2')->asArray()->one(); + unset($article['add_date']); + $this->assertEquals([ + 'id' => '2', + 'author_id' => '2', + 'tag' => '3,4', + ], $article); + + // indexBy + $articles = ArticleIndex::find()->indexBy('author_id')->orderBy('id DESC')->all(); + $this->assertEquals(2, count($articles)); + $this->assertTrue($articles['1'] instanceof ArticleIndex); + $this->assertTrue($articles['2'] instanceof ArticleIndex); + + // indexBy callable + $articles = ArticleIndex::find()->indexBy(function ($article) { + return $article->id . '-' . $article->author_id; + })->orderBy('id DESC')->all(); + $this->assertEquals(2, count($articles)); + $this->assertTrue($articles['1-1'] instanceof ArticleIndex); + $this->assertTrue($articles['2-2'] instanceof ArticleIndex); + } + + public function testFindBySql() + { + // find one + $article = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index ORDER BY id DESC')->one(); + $this->assertTrue($article instanceof ArticleIndex); + $this->assertEquals(2, $article->author_id); + + // find all + $articles = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index')->all(); + $this->assertEquals(2, count($articles)); + + // find with parameter binding + $article = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index WHERE id=:id', [':id' => 2])->one(); + $this->assertTrue($article instanceof ArticleIndex); + $this->assertEquals(2, $article->author_id); + } + + public function testInsert() + { + $record = new RuntimeIndex; + $record->id = 15; + $record->title = 'test title'; + $record->content = 'test content'; + $record->type_id = 7; + $record->category = [1, 2]; + + $this->assertTrue($record->isNewRecord); + + $record->save(); + + $this->assertEquals(15, $record->id); + $this->assertFalse($record->isNewRecord); + } + + /** + * @depends testInsert + */ + public function testUpdate() + { + $record = new RuntimeIndex; + $record->id = 2; + $record->title = 'test title'; + $record->content = 'test content'; + $record->type_id = 7; + $record->category = [1, 2]; + $record->save(); + + // save + $record = RuntimeIndex::find(2); + $this->assertTrue($record instanceof RuntimeIndex); + $this->assertEquals(7, $record->type_id); + $this->assertFalse($record->isNewRecord); + + $record->type_id = 9; + $record->save(); + $this->assertEquals(9, $record->type_id); + $this->assertFalse($record->isNewRecord); + $record2 = RuntimeIndex::find(['id' => 2]); + $this->assertEquals(9, $record2->type_id); + + // replace + $query = 'replace'; + $rows = RuntimeIndex::find()->match($query)->all(); + $this->assertEmpty($rows); + $record = RuntimeIndex::find(2); + $record->content = 'Test content with ' . $query; + $record->save(); + $rows = RuntimeIndex::find()->match($query); + $this->assertNotEmpty($rows); + + // updateAll + $pk = ['id' => 2]; + $ret = RuntimeIndex::updateAll(['type_id' => 55], $pk); + $this->assertEquals(1, $ret); + $record = RuntimeIndex::find($pk); + $this->assertEquals(55, $record->type_id); + } + + /** + * @depends testInsert + */ + public function testDelete() + { + // delete + $record = new RuntimeIndex; + $record->id = 2; + $record->title = 'test title'; + $record->content = 'test content'; + $record->type_id = 7; + $record->category = [1, 2]; + $record->save(); + + $record = RuntimeIndex::find(2); + $record->delete(); + $record = RuntimeIndex::find(2); + $this->assertNull($record); + + // deleteAll + $record = new RuntimeIndex; + $record->id = 2; + $record->title = 'test title'; + $record->content = 'test content'; + $record->type_id = 7; + $record->category = [1, 2]; + $record->save(); + + $ret = RuntimeIndex::deleteAll('id = 2'); + $this->assertEquals(1, $ret); + $records = RuntimeIndex::find()->all(); + $this->assertEquals(0, count($records)); + } + + public function testCallSnippets() + { + $query = 'pencil'; + $source = 'Some data sentence about ' . $query; + + $snippet = ArticleIndex::callSnippets($source, $query); + $this->assertNotEmpty($snippet, 'Unable to call snippets!'); + $this->assertContains('' . $query . '', $snippet, 'Query not present in the snippet!'); + + $rows = ArticleIndex::callSnippets([$source], $query); + $this->assertNotEmpty($rows, 'Unable to call snippets!'); + $this->assertContains('' . $query . '', $rows[0], 'Query not present in the snippet!'); + } + + public function testCallKeywords() + { + $text = 'table pencil'; + $rows = ArticleIndex::callKeywords($text); + $this->assertNotEmpty($rows, 'Unable to call keywords!'); + $this->assertArrayHasKey('tokenized', $rows[0], 'No tokenized keyword!'); + $this->assertArrayHasKey('normalized', $rows[0], 'No normalized keyword!'); + } } diff --git a/tests/unit/extensions/sphinx/ActiveRelationTest.php b/tests/unit/extensions/sphinx/ActiveRelationTest.php index d85c6b9fd1f..4d783d61978 100644 --- a/tests/unit/extensions/sphinx/ActiveRelationTest.php +++ b/tests/unit/extensions/sphinx/ActiveRelationTest.php @@ -12,34 +12,34 @@ */ class ActiveRelationTest extends SphinxTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - ActiveRecordDb::$db = $this->getDbConnection(); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + ActiveRecordDb::$db = $this->getDbConnection(); + } - // Tests : + // Tests : - public function testFindLazy() - { - /** @var ArticleDb $article */ - $article = ArticleDb::find(['id' => 2]); - $this->assertFalse($article->isRelationPopulated('index')); - $index = $article->index; - $this->assertTrue($article->isRelationPopulated('index')); - $this->assertTrue($index instanceof ArticleIndex); - $this->assertEquals(1, count($article->relatedRecords)); - $this->assertEquals($article->id, $index->id); - } + public function testFindLazy() + { + /** @var ArticleDb $article */ + $article = ArticleDb::find(['id' => 2]); + $this->assertFalse($article->isRelationPopulated('index')); + $index = $article->index; + $this->assertTrue($article->isRelationPopulated('index')); + $this->assertTrue($index instanceof ArticleIndex); + $this->assertEquals(1, count($article->relatedRecords)); + $this->assertEquals($article->id, $index->id); + } - public function testFindEager() - { - $articles = ArticleDb::find()->with('index')->all(); - $this->assertEquals(2, count($articles)); - $this->assertTrue($articles[0]->isRelationPopulated('index')); - $this->assertTrue($articles[1]->isRelationPopulated('index')); - $this->assertTrue($articles[0]->index instanceof ArticleIndex); - $this->assertTrue($articles[1]->index instanceof ArticleIndex); - } + public function testFindEager() + { + $articles = ArticleDb::find()->with('index')->all(); + $this->assertEquals(2, count($articles)); + $this->assertTrue($articles[0]->isRelationPopulated('index')); + $this->assertTrue($articles[1]->isRelationPopulated('index')); + $this->assertTrue($articles[0]->index instanceof ArticleIndex); + $this->assertTrue($articles[1]->index instanceof ArticleIndex); + } } diff --git a/tests/unit/extensions/sphinx/ColumnSchemaTest.php b/tests/unit/extensions/sphinx/ColumnSchemaTest.php index 0a5df789c66..88965fc64bb 100644 --- a/tests/unit/extensions/sphinx/ColumnSchemaTest.php +++ b/tests/unit/extensions/sphinx/ColumnSchemaTest.php @@ -9,47 +9,47 @@ */ class ColumnSchemaTest extends SphinxTestCase { - /** - * Data provider for [[testTypeCast]] - * @return array test data. - */ - public function dataProviderTypeCast() - { - return [ - [ - 'integer', - 'integer', - 5, - 5 - ], - [ - 'integer', - 'integer', - '5', - 5 - ], - [ - 'string', - 'string', - 5, - '5' - ], - ]; - } + /** + * Data provider for [[testTypeCast]] + * @return array test data. + */ + public function dataProviderTypeCast() + { + return [ + [ + 'integer', + 'integer', + 5, + 5 + ], + [ + 'integer', + 'integer', + '5', + 5 + ], + [ + 'string', + 'string', + 5, + '5' + ], + ]; + } - /** - * @dataProvider dataProviderTypeCast - * - * @param $type - * @param $phpType - * @param $value - * @param $expectedResult - */ - public function testTypeCast($type, $phpType, $value, $expectedResult) - { - $columnSchema = new ColumnSchema(); - $columnSchema->type = $type; - $columnSchema->phpType = $phpType; - $this->assertEquals($expectedResult, $columnSchema->typecast($value)); - } + /** + * @dataProvider dataProviderTypeCast + * + * @param $type + * @param $phpType + * @param $value + * @param $expectedResult + */ + public function testTypeCast($type, $phpType, $value, $expectedResult) + { + $columnSchema = new ColumnSchema(); + $columnSchema->type = $type; + $columnSchema->phpType = $phpType; + $this->assertEquals($expectedResult, $columnSchema->typecast($value)); + } } diff --git a/tests/unit/extensions/sphinx/CommandTest.php b/tests/unit/extensions/sphinx/CommandTest.php index 294e1bd6f06..9d092316420 100644 --- a/tests/unit/extensions/sphinx/CommandTest.php +++ b/tests/unit/extensions/sphinx/CommandTest.php @@ -9,401 +9,401 @@ */ class CommandTest extends SphinxTestCase { - protected function tearDown() - { - $this->truncateRuntimeIndex('yii2_test_rt_index'); - parent::tearDown(); - } - - // Tests : - - public function testConstruct() - { - $db = $this->getConnection(false); - - // null - $command = $db->createCommand(); - $this->assertEquals(null, $command->sql); - - // string - $sql = 'SELECT * FROM yii2_test_item_index'; - $params = [ - 'name' => 'value' - ]; - $command = $db->createCommand($sql, $params); - $this->assertEquals($sql, $command->sql); - $this->assertEquals($params, $command->params); - } - - public function testGetSetSql() - { - $db = $this->getConnection(false); - - $sql = 'SELECT * FROM yii2_test_item_index'; - $command = $db->createCommand($sql); - $this->assertEquals($sql, $command->sql); - - $sql2 = 'SELECT * FROM yii2_test_item_index'; - $command->sql = $sql2; - $this->assertEquals($sql2, $command->sql); - } - - public function testAutoQuoting() - { - $db = $this->getConnection(false); - - $sql = 'SELECT [[id]], [[t.name]] FROM {{yii2_test_item_index}} t'; - $command = $db->createCommand($sql); - $this->assertEquals("SELECT `id`, `t`.`name` FROM `yii2_test_item_index` t", $command->sql); - } - - public function testPrepareCancel() - { - $db = $this->getConnection(false); - - $command = $db->createCommand('SELECT * FROM yii2_test_item_index'); - $this->assertEquals(null, $command->pdoStatement); - $command->prepare(); - $this->assertNotEquals(null, $command->pdoStatement); - $command->cancel(); - $this->assertEquals(null, $command->pdoStatement); - } - - public function testExecute() - { - $db = $this->getConnection(); - - $sql = 'SELECT COUNT(*) FROM yii2_test_item_index WHERE MATCH(\'wooden\')'; - $command = $db->createCommand($sql); - $this->assertEquals(1, $command->queryScalar()); - - $command = $db->createCommand('bad SQL'); - $this->setExpectedException('\yii\db\Exception'); - $command->execute(); - } - - public function testQuery() - { - $db = $this->getConnection(); - - // query - $sql = 'SELECT * FROM yii2_test_item_index'; - $reader = $db->createCommand($sql)->query(); - $this->assertTrue($reader instanceof DataReader); - - // queryAll - $rows = $db->createCommand('SELECT * FROM yii2_test_item_index')->queryAll(); - $this->assertEquals(2, count($rows)); - $row = $rows[1]; - $this->assertEquals(2, $row['id']); - $this->assertEquals(2, $row['category_id']); - - $rows = $db->createCommand('SELECT * FROM yii2_test_item_index WHERE id=10')->queryAll(); - $this->assertEquals([], $rows); - - // queryOne - $sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC'; - $row = $db->createCommand($sql)->queryOne(); - $this->assertEquals(1, $row['id']); - $this->assertEquals(1, $row['category_id']); - - $sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC'; - $command = $db->createCommand($sql); - $command->prepare(); - $row = $command->queryOne(); - $this->assertEquals(1, $row['id']); - $this->assertEquals(1, $row['category_id']); - - $sql = 'SELECT * FROM yii2_test_item_index WHERE id=10'; - $command = $db->createCommand($sql); - $this->assertFalse($command->queryOne()); - - // queryColumn - $sql = 'SELECT * FROM yii2_test_item_index'; - $column = $db->createCommand($sql)->queryColumn(); - $this->assertEquals(range(1, 2), $column); - - $command = $db->createCommand('SELECT id FROM yii2_test_item_index WHERE id=10'); - $this->assertEquals([], $command->queryColumn()); - - // queryScalar - $sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC'; - $this->assertEquals($db->createCommand($sql)->queryScalar(), 1); - - $sql = 'SELECT id FROM yii2_test_item_index ORDER BY id ASC'; - $command = $db->createCommand($sql); - $command->prepare(); - $this->assertEquals(1, $command->queryScalar()); - - $command = $db->createCommand('SELECT id FROM yii2_test_item_index WHERE id=10'); - $this->assertFalse($command->queryScalar()); - - $command = $db->createCommand('bad SQL'); - $this->setExpectedException('\yii\db\Exception'); - $command->query(); - } - - /** - * @depends testQuery - */ - public function testInsert() - { - $db = $this->getConnection(); - - $command = $db->createCommand()->insert('yii2_test_rt_index', [ - 'title' => 'Test title', - 'content' => 'Test content', - 'type_id' => 2, - 'category' => [1, 2], - 'id' => 1, - ]); - $this->assertEquals(1, $command->execute(), 'Unable to execute insert!'); - - $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals(1, count($rows), 'No row inserted!'); - } - - /** - * @depends testInsert - */ - public function testBatchInsert() - { - $db = $this->getConnection(); - - $command = $db->createCommand()->batchInsert( - 'yii2_test_rt_index', - [ - 'title', - 'content', - 'type_id', - 'category', - 'id', - ], - [ - [ - 'Test title 1', - 'Test content 1', - 1, - [1, 2], - 1, - ], - [ - 'Test title 2', - 'Test content 2', - 2, - [3, 4], - 2, - ], - ] - ); - $this->assertEquals(2, $command->execute(), 'Unable to execute batch insert!'); - - $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals(2, count($rows), 'No rows inserted!'); - } - - /** - * @depends testInsert - */ - public function testReplace() - { - $db = $this->getConnection(); - - $command = $db->createCommand()->replace('yii2_test_rt_index', [ - 'title' => 'Test title', - 'content' => 'Test content', - 'type_id' => 2, - 'category' => [1, 2], - 'id' => 1, - ]); - $this->assertEquals(1, $command->execute(), 'Unable to execute replace!'); - - $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals(1, count($rows), 'No row inserted!'); - - $newTypeId = 5; - $command = $db->createCommand()->replace('yii2_test_rt_index', [ - 'type_id' => $newTypeId, - 'category' => [3, 4], - 'id' => 1, - ]); - $this->assertEquals(1, $command->execute(), 'Unable to update via replace!'); - - list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!'); - } - - /** - * @depends testReplace - */ - public function testBatchReplace() - { - $db = $this->getConnection(); - - $command = $db->createCommand()->batchReplace( - 'yii2_test_rt_index', - [ - 'title', - 'content', - 'type_id', - 'category', - 'id', - ], - [ - [ - 'Test title 1', - 'Test content 1', - 1, - [1, 2], - 1, - ], - [ - 'Test title 2', - 'Test content 2', - 2, - [3, 4], - 2, - ], - ] - ); - $this->assertEquals(2, $command->execute(), 'Unable to execute batch replace!'); - - $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals(2, count($rows), 'No rows inserted!'); - - $newTypeId = 5; - $command = $db->createCommand()->replace('yii2_test_rt_index', [ - 'type_id' => $newTypeId, - 'id' => 1, - ]); - $this->assertEquals(1, $command->execute(), 'Unable to update via replace!'); - list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!'); - } - - /** - * @depends testInsert - */ - public function testUpdate() - { - $db = $this->getConnection(); - - $db->createCommand()->insert('yii2_test_rt_index', [ - 'title' => 'Test title', - 'content' => 'Test content', - 'type_id' => 2, - 'id' => 1, - ])->execute(); - - $newTypeId = 5; - $command = $db->createCommand()->update( - 'yii2_test_rt_index', - [ - 'type_id' => $newTypeId, - 'category' => [3, 4], - ], - 'id = 1' - ); - $this->assertEquals(1, $command->execute(), 'Unable to execute update!'); - - list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!'); - } - - /** - * @depends testUpdate - */ - public function testUpdateWithOptions() - { - $db = $this->getConnection(); - - $db->createCommand()->insert('yii2_test_rt_index', [ - 'title' => 'Test title', - 'content' => 'Test content', - 'type_id' => 2, - 'id' => 1, - ])->execute(); - - $newTypeId = 5; - $command = $db->createCommand()->update( - 'yii2_test_rt_index', - [ - 'type_id' => $newTypeId, - 'non_existing_attribute' => 10, - ], - 'id = 1', - [], - [ - 'ignore_nonexistent_columns' => 1 - ] - ); - $this->assertEquals(1, $command->execute(), 'Unable to execute update!'); - } - - /** - * @depends testInsert - */ - public function testDelete() - { - $db = $this->getConnection(); - - $db->createCommand()->insert('yii2_test_rt_index', [ - 'title' => 'Test title', - 'content' => 'Test content', - 'type_id' => 2, - 'id' => 1, - ])->execute(); - - $command = $db->createCommand()->delete('yii2_test_rt_index', 'id = 1'); - $this->assertEquals(1, $command->execute(), 'Unable to execute delete!'); - - $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); - $this->assertEquals(0, count($rows), 'Unable to delete record!'); - } - - /** - * @depends testQuery - */ - public function testCallSnippets() - { - $db = $this->getConnection(); - - $query = 'pencil'; - $source = 'Some data sentence about ' . $query; - - $rows = $db->createCommand()->callSnippets('yii2_test_item_index', $source, $query)->queryColumn(); - $this->assertNotEmpty($rows, 'Unable to call snippets!'); - $this->assertContains('' . $query . '', $rows[0], 'Query not present in the snippet!'); - - $rows = $db->createCommand()->callSnippets('yii2_test_item_index', [$source], $query)->queryColumn(); - $this->assertNotEmpty($rows, 'Unable to call snippets for array source!'); - - $options = [ - 'before_match' => '[', - 'after_match' => ']', - 'limit' => 20, - ]; - $snippet = $db->createCommand()->callSnippets('yii2_test_item_index', $source, $query, $options)->queryScalar(); - $this->assertContains($options['before_match'] . $query . $options['after_match'], $snippet, 'Unable to apply options!'); - } - - /** - * @depends testQuery - */ - public function testCallKeywords() - { - $db = $this->getConnection(); - - $text = 'table pencil'; - $rows = $db->createCommand()->callKeywords('yii2_test_item_index', $text)->queryAll(); - $this->assertNotEmpty($rows, 'Unable to call keywords!'); - $this->assertArrayHasKey('tokenized', $rows[0], 'No tokenized keyword!'); - $this->assertArrayHasKey('normalized', $rows[0], 'No normalized keyword!'); - - $text = 'table pencil'; - $rows = $db->createCommand()->callKeywords('yii2_test_item_index', $text, true)->queryAll(); - $this->assertNotEmpty($rows, 'Unable to call keywords with statistic!'); - $this->assertArrayHasKey('docs', $rows[0], 'No docs!'); - $this->assertArrayHasKey('hits', $rows[0], 'No hits!'); - } + protected function tearDown() + { + $this->truncateRuntimeIndex('yii2_test_rt_index'); + parent::tearDown(); + } + + // Tests : + + public function testConstruct() + { + $db = $this->getConnection(false); + + // null + $command = $db->createCommand(); + $this->assertEquals(null, $command->sql); + + // string + $sql = 'SELECT * FROM yii2_test_item_index'; + $params = [ + 'name' => 'value' + ]; + $command = $db->createCommand($sql, $params); + $this->assertEquals($sql, $command->sql); + $this->assertEquals($params, $command->params); + } + + public function testGetSetSql() + { + $db = $this->getConnection(false); + + $sql = 'SELECT * FROM yii2_test_item_index'; + $command = $db->createCommand($sql); + $this->assertEquals($sql, $command->sql); + + $sql2 = 'SELECT * FROM yii2_test_item_index'; + $command->sql = $sql2; + $this->assertEquals($sql2, $command->sql); + } + + public function testAutoQuoting() + { + $db = $this->getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{yii2_test_item_index}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT `id`, `t`.`name` FROM `yii2_test_item_index` t", $command->sql); + } + + public function testPrepareCancel() + { + $db = $this->getConnection(false); + + $command = $db->createCommand('SELECT * FROM yii2_test_item_index'); + $this->assertEquals(null, $command->pdoStatement); + $command->prepare(); + $this->assertNotEquals(null, $command->pdoStatement); + $command->cancel(); + $this->assertEquals(null, $command->pdoStatement); + } + + public function testExecute() + { + $db = $this->getConnection(); + + $sql = 'SELECT COUNT(*) FROM yii2_test_item_index WHERE MATCH(\'wooden\')'; + $command = $db->createCommand($sql); + $this->assertEquals(1, $command->queryScalar()); + + $command = $db->createCommand('bad SQL'); + $this->setExpectedException('\yii\db\Exception'); + $command->execute(); + } + + public function testQuery() + { + $db = $this->getConnection(); + + // query + $sql = 'SELECT * FROM yii2_test_item_index'; + $reader = $db->createCommand($sql)->query(); + $this->assertTrue($reader instanceof DataReader); + + // queryAll + $rows = $db->createCommand('SELECT * FROM yii2_test_item_index')->queryAll(); + $this->assertEquals(2, count($rows)); + $row = $rows[1]; + $this->assertEquals(2, $row['id']); + $this->assertEquals(2, $row['category_id']); + + $rows = $db->createCommand('SELECT * FROM yii2_test_item_index WHERE id=10')->queryAll(); + $this->assertEquals([], $rows); + + // queryOne + $sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals(1, $row['id']); + $this->assertEquals(1, $row['category_id']); + + $sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC'; + $command = $db->createCommand($sql); + $command->prepare(); + $row = $command->queryOne(); + $this->assertEquals(1, $row['id']); + $this->assertEquals(1, $row['category_id']); + + $sql = 'SELECT * FROM yii2_test_item_index WHERE id=10'; + $command = $db->createCommand($sql); + $this->assertFalse($command->queryOne()); + + // queryColumn + $sql = 'SELECT * FROM yii2_test_item_index'; + $column = $db->createCommand($sql)->queryColumn(); + $this->assertEquals(range(1, 2), $column); + + $command = $db->createCommand('SELECT id FROM yii2_test_item_index WHERE id=10'); + $this->assertEquals([], $command->queryColumn()); + + // queryScalar + $sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC'; + $this->assertEquals($db->createCommand($sql)->queryScalar(), 1); + + $sql = 'SELECT id FROM yii2_test_item_index ORDER BY id ASC'; + $command = $db->createCommand($sql); + $command->prepare(); + $this->assertEquals(1, $command->queryScalar()); + + $command = $db->createCommand('SELECT id FROM yii2_test_item_index WHERE id=10'); + $this->assertFalse($command->queryScalar()); + + $command = $db->createCommand('bad SQL'); + $this->setExpectedException('\yii\db\Exception'); + $command->query(); + } + + /** + * @depends testQuery + */ + public function testInsert() + { + $db = $this->getConnection(); + + $command = $db->createCommand()->insert('yii2_test_rt_index', [ + 'title' => 'Test title', + 'content' => 'Test content', + 'type_id' => 2, + 'category' => [1, 2], + 'id' => 1, + ]); + $this->assertEquals(1, $command->execute(), 'Unable to execute insert!'); + + $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals(1, count($rows), 'No row inserted!'); + } + + /** + * @depends testInsert + */ + public function testBatchInsert() + { + $db = $this->getConnection(); + + $command = $db->createCommand()->batchInsert( + 'yii2_test_rt_index', + [ + 'title', + 'content', + 'type_id', + 'category', + 'id', + ], + [ + [ + 'Test title 1', + 'Test content 1', + 1, + [1, 2], + 1, + ], + [ + 'Test title 2', + 'Test content 2', + 2, + [3, 4], + 2, + ], + ] + ); + $this->assertEquals(2, $command->execute(), 'Unable to execute batch insert!'); + + $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals(2, count($rows), 'No rows inserted!'); + } + + /** + * @depends testInsert + */ + public function testReplace() + { + $db = $this->getConnection(); + + $command = $db->createCommand()->replace('yii2_test_rt_index', [ + 'title' => 'Test title', + 'content' => 'Test content', + 'type_id' => 2, + 'category' => [1, 2], + 'id' => 1, + ]); + $this->assertEquals(1, $command->execute(), 'Unable to execute replace!'); + + $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals(1, count($rows), 'No row inserted!'); + + $newTypeId = 5; + $command = $db->createCommand()->replace('yii2_test_rt_index', [ + 'type_id' => $newTypeId, + 'category' => [3, 4], + 'id' => 1, + ]); + $this->assertEquals(1, $command->execute(), 'Unable to update via replace!'); + + list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!'); + } + + /** + * @depends testReplace + */ + public function testBatchReplace() + { + $db = $this->getConnection(); + + $command = $db->createCommand()->batchReplace( + 'yii2_test_rt_index', + [ + 'title', + 'content', + 'type_id', + 'category', + 'id', + ], + [ + [ + 'Test title 1', + 'Test content 1', + 1, + [1, 2], + 1, + ], + [ + 'Test title 2', + 'Test content 2', + 2, + [3, 4], + 2, + ], + ] + ); + $this->assertEquals(2, $command->execute(), 'Unable to execute batch replace!'); + + $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals(2, count($rows), 'No rows inserted!'); + + $newTypeId = 5; + $command = $db->createCommand()->replace('yii2_test_rt_index', [ + 'type_id' => $newTypeId, + 'id' => 1, + ]); + $this->assertEquals(1, $command->execute(), 'Unable to update via replace!'); + list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!'); + } + + /** + * @depends testInsert + */ + public function testUpdate() + { + $db = $this->getConnection(); + + $db->createCommand()->insert('yii2_test_rt_index', [ + 'title' => 'Test title', + 'content' => 'Test content', + 'type_id' => 2, + 'id' => 1, + ])->execute(); + + $newTypeId = 5; + $command = $db->createCommand()->update( + 'yii2_test_rt_index', + [ + 'type_id' => $newTypeId, + 'category' => [3, 4], + ], + 'id = 1' + ); + $this->assertEquals(1, $command->execute(), 'Unable to execute update!'); + + list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!'); + } + + /** + * @depends testUpdate + */ + public function testUpdateWithOptions() + { + $db = $this->getConnection(); + + $db->createCommand()->insert('yii2_test_rt_index', [ + 'title' => 'Test title', + 'content' => 'Test content', + 'type_id' => 2, + 'id' => 1, + ])->execute(); + + $newTypeId = 5; + $command = $db->createCommand()->update( + 'yii2_test_rt_index', + [ + 'type_id' => $newTypeId, + 'non_existing_attribute' => 10, + ], + 'id = 1', + [], + [ + 'ignore_nonexistent_columns' => 1 + ] + ); + $this->assertEquals(1, $command->execute(), 'Unable to execute update!'); + } + + /** + * @depends testInsert + */ + public function testDelete() + { + $db = $this->getConnection(); + + $db->createCommand()->insert('yii2_test_rt_index', [ + 'title' => 'Test title', + 'content' => 'Test content', + 'type_id' => 2, + 'id' => 1, + ])->execute(); + + $command = $db->createCommand()->delete('yii2_test_rt_index', 'id = 1'); + $this->assertEquals(1, $command->execute(), 'Unable to execute delete!'); + + $rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll(); + $this->assertEquals(0, count($rows), 'Unable to delete record!'); + } + + /** + * @depends testQuery + */ + public function testCallSnippets() + { + $db = $this->getConnection(); + + $query = 'pencil'; + $source = 'Some data sentence about ' . $query; + + $rows = $db->createCommand()->callSnippets('yii2_test_item_index', $source, $query)->queryColumn(); + $this->assertNotEmpty($rows, 'Unable to call snippets!'); + $this->assertContains('' . $query . '', $rows[0], 'Query not present in the snippet!'); + + $rows = $db->createCommand()->callSnippets('yii2_test_item_index', [$source], $query)->queryColumn(); + $this->assertNotEmpty($rows, 'Unable to call snippets for array source!'); + + $options = [ + 'before_match' => '[', + 'after_match' => ']', + 'limit' => 20, + ]; + $snippet = $db->createCommand()->callSnippets('yii2_test_item_index', $source, $query, $options)->queryScalar(); + $this->assertContains($options['before_match'] . $query . $options['after_match'], $snippet, 'Unable to apply options!'); + } + + /** + * @depends testQuery + */ + public function testCallKeywords() + { + $db = $this->getConnection(); + + $text = 'table pencil'; + $rows = $db->createCommand()->callKeywords('yii2_test_item_index', $text)->queryAll(); + $this->assertNotEmpty($rows, 'Unable to call keywords!'); + $this->assertArrayHasKey('tokenized', $rows[0], 'No tokenized keyword!'); + $this->assertArrayHasKey('normalized', $rows[0], 'No normalized keyword!'); + + $text = 'table pencil'; + $rows = $db->createCommand()->callKeywords('yii2_test_item_index', $text, true)->queryAll(); + $this->assertNotEmpty($rows, 'Unable to call keywords with statistic!'); + $this->assertArrayHasKey('docs', $rows[0], 'No docs!'); + $this->assertArrayHasKey('hits', $rows[0], 'No hits!'); + } } diff --git a/tests/unit/extensions/sphinx/ConnectionTest.php b/tests/unit/extensions/sphinx/ConnectionTest.php index a31b358e060..10bf232ca29 100644 --- a/tests/unit/extensions/sphinx/ConnectionTest.php +++ b/tests/unit/extensions/sphinx/ConnectionTest.php @@ -9,34 +9,34 @@ */ class ConnectionTest extends SphinxTestCase { - public function testConstruct() - { - $connection = $this->getConnection(false); - $params = $this->sphinxConfig; - - $this->assertEquals($params['dsn'], $connection->dsn); - $this->assertEquals($params['username'], $connection->username); - $this->assertEquals($params['password'], $connection->password); - } - - public function testOpenClose() - { - $connection = $this->getConnection(false, false); - - $this->assertFalse($connection->isActive); - $this->assertEquals(null, $connection->pdo); - - $connection->open(); - $this->assertTrue($connection->isActive); - $this->assertTrue($connection->pdo instanceof \PDO); - - $connection->close(); - $this->assertFalse($connection->isActive); - $this->assertEquals(null, $connection->pdo); - - $connection = new Connection; - $connection->dsn = 'unknown::memory:'; - $this->setExpectedException('yii\db\Exception'); - $connection->open(); - } + public function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->sphinxConfig; + + $this->assertEquals($params['dsn'], $connection->dsn); + $this->assertEquals($params['username'], $connection->username); + $this->assertEquals($params['password'], $connection->password); + } + + public function testOpenClose() + { + $connection = $this->getConnection(false, false); + + $this->assertFalse($connection->isActive); + $this->assertEquals(null, $connection->pdo); + + $connection->open(); + $this->assertTrue($connection->isActive); + $this->assertTrue($connection->pdo instanceof \PDO); + + $connection->close(); + $this->assertFalse($connection->isActive); + $this->assertEquals(null, $connection->pdo); + + $connection = new Connection; + $connection->dsn = 'unknown::memory:'; + $this->setExpectedException('yii\db\Exception'); + $connection->open(); + } } diff --git a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php index 3977b26ab09..ae38a0165b8 100644 --- a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php +++ b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php @@ -12,62 +12,62 @@ */ class ExternalActiveRelationTest extends SphinxTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - ActiveRecordDb::$db = $this->getDbConnection(); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + ActiveRecordDb::$db = $this->getDbConnection(); + } - // Tests : + // Tests : - public function testFindLazy() - { - /** @var ArticleIndex $article */ - $article = ArticleIndex::find(['id' => 2]); + public function testFindLazy() + { + /** @var ArticleIndex $article */ + $article = ArticleIndex::find(['id' => 2]); - // has one : - $this->assertFalse($article->isRelationPopulated('source')); - $source = $article->source; - $this->assertTrue($article->isRelationPopulated('source')); - $this->assertTrue($source instanceof ArticleDb); - $this->assertEquals(1, count($article->relatedRecords)); + // has one : + $this->assertFalse($article->isRelationPopulated('source')); + $source = $article->source; + $this->assertTrue($article->isRelationPopulated('source')); + $this->assertTrue($source instanceof ArticleDb); + $this->assertEquals(1, count($article->relatedRecords)); - // has many : - /*$this->assertFalse($article->isRelationPopulated('tags')); - $tags = $article->tags; - $this->assertTrue($article->isRelationPopulated('tags')); - $this->assertEquals(3, count($tags)); - $this->assertTrue($tags[0] instanceof TagDb);*/ - } + // has many : + /*$this->assertFalse($article->isRelationPopulated('tags')); + $tags = $article->tags; + $this->assertTrue($article->isRelationPopulated('tags')); + $this->assertEquals(3, count($tags)); + $this->assertTrue($tags[0] instanceof TagDb);*/ + } - public function testFindEager() - { - // has one : - $articles = ArticleIndex::find()->with('source')->all(); - $this->assertEquals(2, count($articles)); - $this->assertTrue($articles[0]->isRelationPopulated('source')); - $this->assertTrue($articles[1]->isRelationPopulated('source')); - $this->assertTrue($articles[0]->source instanceof ArticleDb); - $this->assertTrue($articles[1]->source instanceof ArticleDb); + public function testFindEager() + { + // has one : + $articles = ArticleIndex::find()->with('source')->all(); + $this->assertEquals(2, count($articles)); + $this->assertTrue($articles[0]->isRelationPopulated('source')); + $this->assertTrue($articles[1]->isRelationPopulated('source')); + $this->assertTrue($articles[0]->source instanceof ArticleDb); + $this->assertTrue($articles[1]->source instanceof ArticleDb); - // has many : - /*$articles = ArticleIndex::find()->with('tags')->all(); - $this->assertEquals(2, count($articles)); - $this->assertTrue($articles[0]->isRelationPopulated('tags')); - $this->assertTrue($articles[1]->isRelationPopulated('tags'));*/ - } + // has many : + /*$articles = ArticleIndex::find()->with('tags')->all(); + $this->assertEquals(2, count($articles)); + $this->assertTrue($articles[0]->isRelationPopulated('tags')); + $this->assertTrue($articles[1]->isRelationPopulated('tags'));*/ + } - /** - * @depends testFindEager - */ - public function testFindWithSnippets() - { - $articles = ArticleIndex::find() - ->match('about') - ->with('source') - ->snippetByModel() - ->all(); - $this->assertEquals(2, count($articles)); - } + /** + * @depends testFindEager + */ + public function testFindWithSnippets() + { + $articles = ArticleIndex::find() + ->match('about') + ->with('source') + ->snippetByModel() + ->all(); + $this->assertEquals(2, count($articles)); + } } diff --git a/tests/unit/extensions/sphinx/QueryTest.php b/tests/unit/extensions/sphinx/QueryTest.php index 62d76e70479..7d8bd6c88be 100644 --- a/tests/unit/extensions/sphinx/QueryTest.php +++ b/tests/unit/extensions/sphinx/QueryTest.php @@ -9,190 +9,190 @@ */ class QueryTest extends SphinxTestCase { - public function testSelect() - { - // default - $query = new Query; - $query->select('*'); - $this->assertEquals(['*'], $query->select); - $this->assertNull($query->distinct); - $this->assertEquals(null, $query->selectOption); - - $query = new Query; - $query->select('id, name', 'something')->distinct(true); - $this->assertEquals(['id', 'name'], $query->select); - $this->assertTrue($query->distinct); - $this->assertEquals('something', $query->selectOption); - } - - public function testFrom() - { - $query = new Query; - $query->from('tbl_user'); - $this->assertEquals(['tbl_user'], $query->from); - } - - public function testMatch() - { - $query = new Query; - $match = 'test match'; - $query->match($match); - $this->assertEquals($match, $query->match); - - $command = $query->createCommand($this->getConnection(false)); - $this->assertContains('MATCH(', $command->getSql(), 'No MATCH operator present!'); - $this->assertContains($match, $command->params, 'No match query among params!'); - } - - public function testWhere() - { - $query = new Query; - $query->where('id = :id', [':id' => 1]); - $this->assertEquals('id = :id', $query->where); - $this->assertEquals([':id' => 1], $query->params); - - $query->andWhere('name = :name', [':name' => 'something']); - $this->assertEquals(['and', 'id = :id', 'name = :name'], $query->where); - $this->assertEquals([':id' => 1, ':name' => 'something'], $query->params); - - $query->orWhere('age = :age', [':age' => '30']); - $this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->where); - $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); - } - - public function testGroup() - { - $query = new Query; - $query->groupBy('team'); - $this->assertEquals(['team'], $query->groupBy); - - $query->addGroupBy('company'); - $this->assertEquals(['team', 'company'], $query->groupBy); - - $query->addGroupBy('age'); - $this->assertEquals(['team', 'company', 'age'], $query->groupBy); - } - - public function testOrder() - { - $query = new Query; - $query->orderBy('team'); - $this->assertEquals(['team' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('company'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('age'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); - - $query->addOrderBy(['age' => SORT_DESC]); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); - - $query->addOrderBy('age ASC, company DESC'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); - } - - public function testLimitOffset() - { - $query = new Query; - $query->limit(10)->offset(5); - $this->assertEquals(10, $query->limit); - $this->assertEquals(5, $query->offset); - } - - public function testWithin() - { - $query = new Query; - $query->within('team'); - $this->assertEquals(['team' => SORT_ASC], $query->within); - - $query->addWithin('company'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->within); - - $query->addWithin('age'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->within); - - $query->addWithin(['age' => SORT_DESC]); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->within); - - $query->addWithin('age ASC, company DESC'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->within); - } - - public function testOptions() - { - $query = new Query; - $options = [ - 'cutoff' => 50, - 'max_matches' => 50, - ]; - $query->options($options); - $this->assertEquals($options, $query->options); - - $newMaxMatches = $options['max_matches'] + 10; - $query->addOptions(['max_matches' => $newMaxMatches]); - $this->assertEquals($newMaxMatches, $query->options['max_matches']); - } - - public function testRun() - { - $connection = $this->getConnection(); - - $query = new Query; - $rows = $query->from('yii2_test_article_index') - ->match('about') - ->options([ - 'cutoff' => 50, - 'field_weights' => [ - 'title' => 10, - 'content' => 3, - ], - ]) - ->all($connection); - $this->assertNotEmpty($rows); - } - - /** - * @depends testRun - */ - public function testSnippet() - { - $connection = $this->getConnection(); - - $match = 'about'; - $snippetPrefix = 'snippet#'; - $snippetCallback = function () use ($match, $snippetPrefix) { - return [ - $snippetPrefix . '1: ' . $match, - $snippetPrefix . '2: ' . $match, - ]; - }; - $snippetOptions = [ - 'before_match' => '[', - 'after_match' => ']', - ]; - - $query = new Query; - $rows = $query->from('yii2_test_article_index') - ->match($match) - ->snippetCallback($snippetCallback) - ->snippetOptions($snippetOptions) - ->all($connection); - $this->assertNotEmpty($rows); - foreach ($rows as $row) { - $this->assertContains($snippetPrefix, $row['snippet'], 'Snippet source not present!'); - $this->assertContains($snippetOptions['before_match'] . $match, $row['snippet'] . $snippetOptions['after_match'], 'Options not applied!'); - } - } - - public function testCount() - { - $connection = $this->getConnection(); - - $query = new Query; - $count = $query->from('yii2_test_article_index') - ->match('about') - ->count('*', $connection); - $this->assertEquals(2, $count); - } + public function testSelect() + { + // default + $query = new Query; + $query->select('*'); + $this->assertEquals(['*'], $query->select); + $this->assertNull($query->distinct); + $this->assertEquals(null, $query->selectOption); + + $query = new Query; + $query->select('id, name', 'something')->distinct(true); + $this->assertEquals(['id', 'name'], $query->select); + $this->assertTrue($query->distinct); + $this->assertEquals('something', $query->selectOption); + } + + public function testFrom() + { + $query = new Query; + $query->from('tbl_user'); + $this->assertEquals(['tbl_user'], $query->from); + } + + public function testMatch() + { + $query = new Query; + $match = 'test match'; + $query->match($match); + $this->assertEquals($match, $query->match); + + $command = $query->createCommand($this->getConnection(false)); + $this->assertContains('MATCH(', $command->getSql(), 'No MATCH operator present!'); + $this->assertContains($match, $command->params, 'No match query among params!'); + } + + public function testWhere() + { + $query = new Query; + $query->where('id = :id', [':id' => 1]); + $this->assertEquals('id = :id', $query->where); + $this->assertEquals([':id' => 1], $query->params); + + $query->andWhere('name = :name', [':name' => 'something']); + $this->assertEquals(['and', 'id = :id', 'name = :name'], $query->where); + $this->assertEquals([':id' => 1, ':name' => 'something'], $query->params); + + $query->orWhere('age = :age', [':age' => '30']); + $this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->where); + $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); + } + + public function testGroup() + { + $query = new Query; + $query->groupBy('team'); + $this->assertEquals(['team'], $query->groupBy); + + $query->addGroupBy('company'); + $this->assertEquals(['team', 'company'], $query->groupBy); + + $query->addGroupBy('age'); + $this->assertEquals(['team', 'company', 'age'], $query->groupBy); + } + + public function testOrder() + { + $query = new Query; + $query->orderBy('team'); + $this->assertEquals(['team' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('company'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('age'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); + + $query->addOrderBy(['age' => SORT_DESC]); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); + + $query->addOrderBy('age ASC, company DESC'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); + } + + public function testLimitOffset() + { + $query = new Query; + $query->limit(10)->offset(5); + $this->assertEquals(10, $query->limit); + $this->assertEquals(5, $query->offset); + } + + public function testWithin() + { + $query = new Query; + $query->within('team'); + $this->assertEquals(['team' => SORT_ASC], $query->within); + + $query->addWithin('company'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->within); + + $query->addWithin('age'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->within); + + $query->addWithin(['age' => SORT_DESC]); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->within); + + $query->addWithin('age ASC, company DESC'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->within); + } + + public function testOptions() + { + $query = new Query; + $options = [ + 'cutoff' => 50, + 'max_matches' => 50, + ]; + $query->options($options); + $this->assertEquals($options, $query->options); + + $newMaxMatches = $options['max_matches'] + 10; + $query->addOptions(['max_matches' => $newMaxMatches]); + $this->assertEquals($newMaxMatches, $query->options['max_matches']); + } + + public function testRun() + { + $connection = $this->getConnection(); + + $query = new Query; + $rows = $query->from('yii2_test_article_index') + ->match('about') + ->options([ + 'cutoff' => 50, + 'field_weights' => [ + 'title' => 10, + 'content' => 3, + ], + ]) + ->all($connection); + $this->assertNotEmpty($rows); + } + + /** + * @depends testRun + */ + public function testSnippet() + { + $connection = $this->getConnection(); + + $match = 'about'; + $snippetPrefix = 'snippet#'; + $snippetCallback = function () use ($match, $snippetPrefix) { + return [ + $snippetPrefix . '1: ' . $match, + $snippetPrefix . '2: ' . $match, + ]; + }; + $snippetOptions = [ + 'before_match' => '[', + 'after_match' => ']', + ]; + + $query = new Query; + $rows = $query->from('yii2_test_article_index') + ->match($match) + ->snippetCallback($snippetCallback) + ->snippetOptions($snippetOptions) + ->all($connection); + $this->assertNotEmpty($rows); + foreach ($rows as $row) { + $this->assertContains($snippetPrefix, $row['snippet'], 'Snippet source not present!'); + $this->assertContains($snippetOptions['before_match'] . $match, $row['snippet'] . $snippetOptions['after_match'], 'Options not applied!'); + } + } + + public function testCount() + { + $connection = $this->getConnection(); + + $query = new Query; + $count = $query->from('yii2_test_article_index') + ->match('about') + ->count('*', $connection); + $this->assertEquals(2, $count); + } } diff --git a/tests/unit/extensions/sphinx/SchemaTest.php b/tests/unit/extensions/sphinx/SchemaTest.php index 63f4f1d38c5..38d8c44c2b4 100644 --- a/tests/unit/extensions/sphinx/SchemaTest.php +++ b/tests/unit/extensions/sphinx/SchemaTest.php @@ -9,75 +9,75 @@ */ class SchemaTest extends SphinxTestCase { - public function testFindIndexNames() - { - $schema = $this->getConnection()->schema; + public function testFindIndexNames() + { + $schema = $this->getConnection()->schema; - $indexes = $schema->getIndexNames(); - $this->assertContains('yii2_test_article_index', $indexes); - $this->assertContains('yii2_test_item_index', $indexes); - $this->assertContains('yii2_test_rt_index', $indexes); - } + $indexes = $schema->getIndexNames(); + $this->assertContains('yii2_test_article_index', $indexes); + $this->assertContains('yii2_test_item_index', $indexes); + $this->assertContains('yii2_test_rt_index', $indexes); + } - public function testGetIndexSchemas() - { - $schema = $this->getConnection()->schema; + public function testGetIndexSchemas() + { + $schema = $this->getConnection()->schema; - $indexes = $schema->getIndexSchemas(); - $this->assertEquals(count($schema->getIndexNames()), count($indexes)); - foreach ($indexes as $index) { - $this->assertInstanceOf('yii\sphinx\IndexSchema', $index); - } - } + $indexes = $schema->getIndexSchemas(); + $this->assertEquals(count($schema->getIndexNames()), count($indexes)); + foreach ($indexes as $index) { + $this->assertInstanceOf('yii\sphinx\IndexSchema', $index); + } + } - public function testGetNonExistingIndexSchema() - { - $this->assertNull($this->getConnection()->schema->getIndexSchema('non_existing_index')); - } + public function testGetNonExistingIndexSchema() + { + $this->assertNull($this->getConnection()->schema->getIndexSchema('non_existing_index')); + } - public function testSchemaRefresh() - { - $schema = $this->getConnection()->schema; + public function testSchemaRefresh() + { + $schema = $this->getConnection()->schema; - $schema->db->enableSchemaCache = true; - $schema->db->schemaCache = new FileCache(); - $noCacheIndex = $schema->getIndexSchema('yii2_test_rt_index', true); - $cachedIndex = $schema->getIndexSchema('yii2_test_rt_index', true); - $this->assertEquals($noCacheIndex, $cachedIndex); - } + $schema->db->enableSchemaCache = true; + $schema->db->schemaCache = new FileCache(); + $noCacheIndex = $schema->getIndexSchema('yii2_test_rt_index', true); + $cachedIndex = $schema->getIndexSchema('yii2_test_rt_index', true); + $this->assertEquals($noCacheIndex, $cachedIndex); + } - public function testGetPDOType() - { - $values = [ - [null, \PDO::PARAM_NULL], - ['', \PDO::PARAM_STR], - ['hello', \PDO::PARAM_STR], - [0, \PDO::PARAM_INT], - [1, \PDO::PARAM_INT], - [1337, \PDO::PARAM_INT], - [true, \PDO::PARAM_BOOL], - [false, \PDO::PARAM_BOOL], - [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], - ]; + public function testGetPDOType() + { + $values = [ + [null, \PDO::PARAM_NULL], + ['', \PDO::PARAM_STR], + ['hello', \PDO::PARAM_STR], + [0, \PDO::PARAM_INT], + [1, \PDO::PARAM_INT], + [1337, \PDO::PARAM_INT], + [true, \PDO::PARAM_BOOL], + [false, \PDO::PARAM_BOOL], + [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], + ]; - $schema = $this->getConnection()->schema; + $schema = $this->getConnection()->schema; - foreach ($values as $value) { - $this->assertEquals($value[1], $schema->getPdoType($value[0])); - } - fclose($fp); - } + foreach ($values as $value) { + $this->assertEquals($value[1], $schema->getPdoType($value[0])); + } + fclose($fp); + } - public function testIndexType() - { - $schema = $this->getConnection()->schema; + public function testIndexType() + { + $schema = $this->getConnection()->schema; - $index = $schema->getIndexSchema('yii2_test_article_index'); - $this->assertEquals('local', $index->type); - $this->assertFalse($index->isRuntime); + $index = $schema->getIndexSchema('yii2_test_article_index'); + $this->assertEquals('local', $index->type); + $this->assertFalse($index->isRuntime); - $index = $schema->getIndexSchema('yii2_test_rt_index'); - $this->assertEquals('rt', $index->type); - $this->assertTrue($index->isRuntime); - } + $index = $schema->getIndexSchema('yii2_test_rt_index'); + $this->assertEquals('rt', $index->type); + $this->assertTrue($index->isRuntime); + } } diff --git a/tests/unit/extensions/sphinx/SphinxTestCase.php b/tests/unit/extensions/sphinx/SphinxTestCase.php index 1c756cc3dd5..6c1e5e0323f 100644 --- a/tests/unit/extensions/sphinx/SphinxTestCase.php +++ b/tests/unit/extensions/sphinx/SphinxTestCase.php @@ -12,143 +12,145 @@ */ class SphinxTestCase extends TestCase { - /** - * @var array Sphinx connection configuration. - */ - protected $sphinxConfig = [ - 'dsn' => 'mysql:host=127.0.0.1;port=9306;', - 'username' => '', - 'password' => '', - ]; - /** - * @var Connection Sphinx connection instance. - */ - protected $sphinx; - /** - * @var array Database connection configuration. - */ - protected $dbConfig = [ - 'dsn' => 'mysql:host=127.0.0.1;', - 'username' => '', - 'password' => '', - ]; - /** - * @var \yii\db\Connection database connection instance. - */ - protected $db; + /** + * @var array Sphinx connection configuration. + */ + protected $sphinxConfig = [ + 'dsn' => 'mysql:host=127.0.0.1;port=9306;', + 'username' => '', + 'password' => '', + ]; + /** + * @var Connection Sphinx connection instance. + */ + protected $sphinx; + /** + * @var array Database connection configuration. + */ + protected $dbConfig = [ + 'dsn' => 'mysql:host=127.0.0.1;', + 'username' => '', + 'password' => '', + ]; + /** + * @var \yii\db\Connection database connection instance. + */ + protected $db; - public static function setUpBeforeClass() - { - static::loadClassMap(); - } + public static function setUpBeforeClass() + { + static::loadClassMap(); + } - protected function setUp() - { - parent::setUp(); - if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { - $this->markTestSkipped('pdo and pdo_mysql extension are required.'); - } - $config = $this->getParam('sphinx'); - if (!empty($config)) { - $this->sphinxConfig = $config['sphinx']; - $this->dbConfig = $config['db']; - } - $this->mockApplication(); - static::loadClassMap(); - } + protected function setUp() + { + parent::setUp(); + if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { + $this->markTestSkipped('pdo and pdo_mysql extension are required.'); + } + $config = $this->getParam('sphinx'); + if (!empty($config)) { + $this->sphinxConfig = $config['sphinx']; + $this->dbConfig = $config['db']; + } + $this->mockApplication(); + static::loadClassMap(); + } - protected function tearDown() - { - if ($this->sphinx) { - $this->sphinx->close(); - } - $this->destroyApplication(); - } + protected function tearDown() + { + if ($this->sphinx) { + $this->sphinx->close(); + } + $this->destroyApplication(); + } - /** - * Adds sphinx extension files to [[Yii::$classPath]], - * avoiding the necessity of usage Composer autoloader. - */ - protected static function loadClassMap() - { - $baseNameSpace = 'yii/sphinx'; - $basePath = realpath(__DIR__. '/../../../../extensions/sphinx'); - $files = FileHelper::findFiles($basePath); - foreach ($files as $file) { - $classRelativePath = str_replace($basePath, '', $file); - $classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath); - Yii::$classMap[$classFullName] = $file; - } - } + /** + * Adds sphinx extension files to [[Yii::$classPath]], + * avoiding the necessity of usage Composer autoloader. + */ + protected static function loadClassMap() + { + $baseNameSpace = 'yii/sphinx'; + $basePath = realpath(__DIR__. '/../../../../extensions/sphinx'); + $files = FileHelper::findFiles($basePath); + foreach ($files as $file) { + $classRelativePath = str_replace($basePath, '', $file); + $classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath); + Yii::$classMap[$classFullName] = $file; + } + } - /** - * @param boolean $reset whether to clean up the test database - * @param boolean $open whether to open test database - * @return \yii\sphinx\Connection - */ - public function getConnection($reset = false, $open = true) - { - if (!$reset && $this->sphinx) { - return $this->sphinx; - } - $db = new Connection; - $db->dsn = $this->sphinxConfig['dsn']; - if (isset($this->sphinxConfig['username'])) { - $db->username = $this->sphinxConfig['username']; - $db->password = $this->sphinxConfig['password']; - } - if (isset($this->sphinxConfig['attributes'])) { - $db->attributes = $this->sphinxConfig['attributes']; - } - if ($open) { - $db->open(); - } - $this->sphinx = $db; - return $db; - } + /** + * @param boolean $reset whether to clean up the test database + * @param boolean $open whether to open test database + * @return \yii\sphinx\Connection + */ + public function getConnection($reset = false, $open = true) + { + if (!$reset && $this->sphinx) { + return $this->sphinx; + } + $db = new Connection; + $db->dsn = $this->sphinxConfig['dsn']; + if (isset($this->sphinxConfig['username'])) { + $db->username = $this->sphinxConfig['username']; + $db->password = $this->sphinxConfig['password']; + } + if (isset($this->sphinxConfig['attributes'])) { + $db->attributes = $this->sphinxConfig['attributes']; + } + if ($open) { + $db->open(); + } + $this->sphinx = $db; - /** - * Truncates the runtime index. - * @param string $indexName index name. - */ - protected function truncateRuntimeIndex($indexName) - { - if ($this->sphinx) { - $this->sphinx->createCommand('TRUNCATE RTINDEX ' . $indexName)->execute(); - } - } + return $db; + } - /** - * @param boolean $reset whether to clean up the test database - * @param boolean $open whether to open and populate test database - * @return \yii\db\Connection - */ - public function getDbConnection($reset = true, $open = true) - { - if (!$reset && $this->db) { - return $this->db; - } - $db = new \yii\db\Connection; - $db->dsn = $this->dbConfig['dsn']; - if (isset($this->dbConfig['username'])) { - $db->username = $this->dbConfig['username']; - $db->password = $this->dbConfig['password']; - } - if (isset($this->dbConfig['attributes'])) { - $db->attributes = $this->dbConfig['attributes']; - } - if ($open) { - $db->open(); - if (!empty($this->dbConfig['fixture'])) { - $lines = explode(';', file_get_contents($this->dbConfig['fixture'])); - foreach ($lines as $line) { - if (trim($line) !== '') { - $db->pdo->exec($line); - } - } - } - } - $this->db = $db; - return $db; - } + /** + * Truncates the runtime index. + * @param string $indexName index name. + */ + protected function truncateRuntimeIndex($indexName) + { + if ($this->sphinx) { + $this->sphinx->createCommand('TRUNCATE RTINDEX ' . $indexName)->execute(); + } + } + + /** + * @param boolean $reset whether to clean up the test database + * @param boolean $open whether to open and populate test database + * @return \yii\db\Connection + */ + public function getDbConnection($reset = true, $open = true) + { + if (!$reset && $this->db) { + return $this->db; + } + $db = new \yii\db\Connection; + $db->dsn = $this->dbConfig['dsn']; + if (isset($this->dbConfig['username'])) { + $db->username = $this->dbConfig['username']; + $db->password = $this->dbConfig['password']; + } + if (isset($this->dbConfig['attributes'])) { + $db->attributes = $this->dbConfig['attributes']; + } + if ($open) { + $db->open(); + if (!empty($this->dbConfig['fixture'])) { + $lines = explode(';', file_get_contents($this->dbConfig['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + } + $this->db = $db; + + return $db; + } } diff --git a/tests/unit/extensions/swiftmailer/MailerTest.php b/tests/unit/extensions/swiftmailer/MailerTest.php index be3f464d4d7..e5236bf8bd2 100644 --- a/tests/unit/extensions/swiftmailer/MailerTest.php +++ b/tests/unit/extensions/swiftmailer/MailerTest.php @@ -15,110 +15,111 @@ */ class MailerTest extends VendorTestCase { - public function setUp() - { - $this->mockApplication([ - 'components' => [ - 'email' => $this->createTestEmailComponent() - ] - ]); - } - - /** - * @return Mailer test email component instance. - */ - protected function createTestEmailComponent() - { - $component = new Mailer(); - return $component; - } - - // Tests : - - public function testSetupTransport() - { - $mailer = new Mailer(); - - $transport = \Swift_MailTransport::newInstance(); - $mailer->setTransport($transport); - $this->assertEquals($transport, $mailer->getTransport(), 'Unable to setup transport!'); - } - - /** - * @depends testSetupTransport - */ - public function testConfigureTransport() - { - $mailer = new Mailer(); - - $transportConfig = [ - 'class' => 'Swift_SmtpTransport', - 'host' => 'localhost', - 'username' => 'username', - 'password' => 'password', - ]; - $mailer->setTransport($transportConfig); - $transport = $mailer->getTransport(); - $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); - $this->assertEquals($transportConfig['class'], get_class($transport), 'Invalid transport class!'); - $this->assertEquals($transportConfig['host'], $transport->getHost(), 'Invalid transport host!'); - } - - /** - * @depends testConfigureTransport - */ - public function testConfigureTransportConstruct() - { - $mailer = new Mailer(); - - $class = 'Swift_SmtpTransport'; - $host = 'some.test.host'; - $port = 999; - $transportConfig = [ - 'class' => $class, - 'constructArgs' => [ - $host, - $port, - ], - ]; - $mailer->setTransport($transportConfig); - $transport = $mailer->getTransport(); - $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); - $this->assertEquals($class, get_class($transport), 'Invalid transport class!'); - $this->assertEquals($host, $transport->getHost(), 'Invalid transport host!'); - $this->assertEquals($port, $transport->getPort(), 'Invalid transport host!'); - } - - /** - * @depends testConfigureTransportConstruct - */ - public function testConfigureTransportWithPlugins() - { - $mailer = new Mailer(); - - $pluginClass = 'Swift_Plugins_ThrottlerPlugin'; - $rate = 10; - - $transportConfig = [ - 'class' => 'Swift_SmtpTransport', - 'plugins' => [ - [ - 'class' => $pluginClass, - 'constructArgs' => [ - $rate, - ], - ], - ], - ]; - $mailer->setTransport($transportConfig); - $transport = $mailer->getTransport(); - $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); - $this->assertContains(':' . $pluginClass . ':', print_r($transport, true), 'Plugin not added'); - } - - public function testGetSwiftMailer() - { - $mailer = new Mailer(); - $this->assertTrue(is_object($mailer->getSwiftMailer()), 'Unable to get Swift mailer instance!'); - } + public function setUp() + { + $this->mockApplication([ + 'components' => [ + 'email' => $this->createTestEmailComponent() + ] + ]); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer(); + + return $component; + } + + // Tests : + + public function testSetupTransport() + { + $mailer = new Mailer(); + + $transport = \Swift_MailTransport::newInstance(); + $mailer->setTransport($transport); + $this->assertEquals($transport, $mailer->getTransport(), 'Unable to setup transport!'); + } + + /** + * @depends testSetupTransport + */ + public function testConfigureTransport() + { + $mailer = new Mailer(); + + $transportConfig = [ + 'class' => 'Swift_SmtpTransport', + 'host' => 'localhost', + 'username' => 'username', + 'password' => 'password', + ]; + $mailer->setTransport($transportConfig); + $transport = $mailer->getTransport(); + $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); + $this->assertEquals($transportConfig['class'], get_class($transport), 'Invalid transport class!'); + $this->assertEquals($transportConfig['host'], $transport->getHost(), 'Invalid transport host!'); + } + + /** + * @depends testConfigureTransport + */ + public function testConfigureTransportConstruct() + { + $mailer = new Mailer(); + + $class = 'Swift_SmtpTransport'; + $host = 'some.test.host'; + $port = 999; + $transportConfig = [ + 'class' => $class, + 'constructArgs' => [ + $host, + $port, + ], + ]; + $mailer->setTransport($transportConfig); + $transport = $mailer->getTransport(); + $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); + $this->assertEquals($class, get_class($transport), 'Invalid transport class!'); + $this->assertEquals($host, $transport->getHost(), 'Invalid transport host!'); + $this->assertEquals($port, $transport->getPort(), 'Invalid transport host!'); + } + + /** + * @depends testConfigureTransportConstruct + */ + public function testConfigureTransportWithPlugins() + { + $mailer = new Mailer(); + + $pluginClass = 'Swift_Plugins_ThrottlerPlugin'; + $rate = 10; + + $transportConfig = [ + 'class' => 'Swift_SmtpTransport', + 'plugins' => [ + [ + 'class' => $pluginClass, + 'constructArgs' => [ + $rate, + ], + ], + ], + ]; + $mailer->setTransport($transportConfig); + $transport = $mailer->getTransport(); + $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); + $this->assertContains(':' . $pluginClass . ':', print_r($transport, true), 'Plugin not added'); + } + + public function testGetSwiftMailer() + { + $mailer = new Mailer(); + $this->assertTrue(is_object($mailer->getSwiftMailer()), 'Unable to get Swift mailer instance!'); + } } diff --git a/tests/unit/extensions/swiftmailer/MessageTest.php b/tests/unit/extensions/swiftmailer/MessageTest.php index e9ecc9f9d39..f51ee239714 100644 --- a/tests/unit/extensions/swiftmailer/MessageTest.php +++ b/tests/unit/extensions/swiftmailer/MessageTest.php @@ -17,347 +17,350 @@ */ class MessageTest extends VendorTestCase { - /** - * @var string test email address, which will be used as receiver for the messages. - */ - protected $testEmailReceiver = 'someuser@somedomain.com'; - - public function setUp() - { - $this->mockApplication([ - 'components' => [ - 'mail' => $this->createTestEmailComponent() - ] - ]); - $filePath = $this->getTestFilePath(); - if (!file_exists($filePath)) { - FileHelper::createDirectory($filePath); - } - } - - public function tearDown() - { - $filePath = $this->getTestFilePath(); - if (file_exists($filePath)) { - FileHelper::removeDirectory($filePath); - } - } - - /** - * @return string test file path. - */ - protected function getTestFilePath() - { - return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid(); - } - - /** - * @return Mailer test email component instance. - */ - protected function createTestEmailComponent() - { - $component = new Mailer([ - 'useFileTransport' => true, - ]); - return $component; - } - - /** - * @return Message test message instance. - */ - protected function createTestMessage() - { - return Yii::$app->getComponent('mail')->compose(); - } - - /** - * Creates image file with given text. - * @param string $fileName file name. - * @param string $text text to be applied on image. - * @return string image file full name. - */ - protected function createImageFile($fileName = 'test.jpg', $text = 'Test Image') - { - if (!function_exists('imagecreatetruecolor')) { - $this->markTestSkipped('GD lib required.'); - } - $fileFullName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $fileName; - $image = imagecreatetruecolor(120, 20); - $textColor = imagecolorallocate($image, 233, 14, 91); - imagestring($image, 1, 5, 5, $text, $textColor); - imagejpeg($image, $fileFullName); - imagedestroy($image); - return $fileFullName; - } - - /** - * Finds the attachment object in the message. - * @param Message $message message instance - * @return null|\Swift_Mime_Attachment attachment instance. - */ - protected function getAttachment(Message $message) - { - $messageParts = $message->getSwiftMessage()->getChildren(); - $attachment = null; - foreach ($messageParts as $part) { - if ($part instanceof \Swift_Mime_Attachment) { - $attachment = $part; - break; - } - } - return $attachment; - } - - // Tests : - - public function testGetSwiftMessage() - { - $message = new Message(); - $this->assertTrue(is_object($message->getSwiftMessage()), 'Unable to get Swift message!'); - } - - /** - * @depends testGetSwiftMessage - */ - public function testSetGet() - { - $message = new Message(); - - $charset = 'utf-16'; - $message->setCharset($charset); - $this->assertEquals($charset, $message->getCharset(), 'Unable to set charset!'); - - $subject = 'Test Subject'; - $message->setSubject($subject); - $this->assertEquals($subject, $message->getSubject(), 'Unable to set subject!'); - - $from = 'from@somedomain.com'; - $message->setFrom($from); - $this->assertContains($from, array_keys($message->getFrom()), 'Unable to set from!'); - - $replyTo = 'reply-to@somedomain.com'; - $message->setReplyTo($replyTo); - $this->assertContains($replyTo, array_keys($message->getReplyTo()), 'Unable to set replyTo!'); - - $to = 'someuser@somedomain.com'; - $message->setTo($to); - $this->assertContains($to, array_keys($message->getTo()), 'Unable to set to!'); - - $cc = 'ccuser@somedomain.com'; - $message->setCc($cc); - $this->assertContains($cc, array_keys($message->getCc()), 'Unable to set cc!'); - - $bcc = 'bccuser@somedomain.com'; - $message->setBcc($bcc); - $this->assertContains($bcc, array_keys($message->getBcc()), 'Unable to set bcc!'); - } - - /** - * @depends testGetSwiftMessage - */ - public function testSetupHeaders() - { - $charset = 'utf-16'; - $subject = 'Test Subject'; - $from = 'from@somedomain.com'; - $replyTo = 'reply-to@somedomain.com'; - $to = 'someuser@somedomain.com'; - $cc = 'ccuser@somedomain.com'; - $bcc = 'bccuser@somedomain.com'; - - $messageString = $this->createTestMessage() - ->setCharset($charset) - ->setSubject($subject) - ->setFrom($from) - ->setReplyTo($replyTo) - ->setTo($to) - ->setCc($cc) - ->setBcc($bcc) - ->toString(); - - $this->assertContains('charset=' . $charset, $messageString, 'Incorrect charset!'); - $this->assertContains('Subject: ' . $subject, $messageString, 'Incorrect "Subject" header!'); - $this->assertContains('From: ' . $from, $messageString, 'Incorrect "From" header!'); - $this->assertContains('Reply-To: ' . $replyTo, $messageString, 'Incorrect "Reply-To" header!'); - $this->assertContains('To: ' . $to, $messageString, 'Incorrect "To" header!'); - $this->assertContains('Cc: ' . $cc, $messageString, 'Incorrect "Cc" header!'); - $this->assertContains('Bcc: ' . $bcc, $messageString, 'Incorrect "Bcc" header!'); - } - - /** - * @depends testGetSwiftMessage - */ - public function testSend() - { - $message = $this->createTestMessage(); - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Test'); - $message->setTextBody('Yii Swift Test body'); - $this->assertTrue($message->send()); - } - - /** - * @depends testSend - */ - public function testAttachFile() - { - $message = $this->createTestMessage(); - - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Attach File Test'); - $message->setTextBody('Yii Swift Attach File Test body'); - $fileName = __FILE__; - $message->attach($fileName); - - $this->assertTrue($message->send()); - - $attachment = $this->getAttachment($message); - $this->assertTrue(is_object($attachment), 'No attachment found!'); - $this->assertContains($attachment->getFilename(), $fileName, 'Invalid file name!'); - } - - /** - * @depends testSend - */ - public function testAttachContent() - { - $message = $this->createTestMessage(); - - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Create Attachment Test'); - $message->setTextBody('Yii Swift Create Attachment Test body'); - $fileName = 'test.txt'; - $fileContent = 'Test attachment content'; - $message->attachContent($fileContent, ['fileName' => $fileName]); - - $this->assertTrue($message->send()); - - $attachment = $this->getAttachment($message); - $this->assertTrue(is_object($attachment), 'No attachment found!'); - $this->assertEquals($fileName, $attachment->getFilename(), 'Invalid file name!'); - } - - /** - * @depends testSend - */ - public function testEmbedFile() - { - $fileName = $this->createImageFile('embed_file.jpg', 'Embed Image File'); - - $message = $this->createTestMessage(); - - $cid = $message->embed($fileName); - - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Embed File Test'); - $message->setHtmlBody('Embed image: pic'); - - $this->assertTrue($message->send()); - - $attachment = $this->getAttachment($message); - $this->assertTrue(is_object($attachment), 'No attachment found!'); - $this->assertContains($attachment->getFilename(), $fileName, 'Invalid file name!'); - } - - /** - * @depends testSend - */ - public function testEmbedContent() - { - $fileFullName = $this->createImageFile('embed_file.jpg', 'Embed Image File'); - $message = $this->createTestMessage(); - - $fileName = basename($fileFullName); - $contentType = 'image/jpeg'; - $fileContent = file_get_contents($fileFullName); - - $cid = $message->embedContent($fileContent, ['fileName' => $fileName, 'contentType' => $contentType]); - - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Embed File Test'); - $message->setHtmlBody('Embed image: pic'); - - $this->assertTrue($message->send()); - - $attachment = $this->getAttachment($message); - $this->assertTrue(is_object($attachment), 'No attachment found!'); - $this->assertEquals($fileName, $attachment->getFilename(), 'Invalid file name!'); - $this->assertEquals($contentType, $attachment->getContentType(), 'Invalid content type!'); - } - - /** - * @depends testSend - */ - public function testSendAlternativeBody() - { - $message = $this->createTestMessage(); - - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Alternative Body Test'); - $message->setHtmlBody('Yii Swift test HTML body'); - $message->setTextBody('Yii Swift test plain text body'); - - $this->assertTrue($message->send()); - - $messageParts = $message->getSwiftMessage()->getChildren(); - $textPresent = false; - $htmlPresent = false; - foreach ($messageParts as $part) { - if (!($part instanceof \Swift_Mime_Attachment)) { - /* @var \Swift_Mime_MimePart $part */ - if ($part->getContentType() == 'text/plain') { - $textPresent = true; - } - if ($part->getContentType() == 'text/html') { - $htmlPresent = true; - } - } - } - $this->assertTrue($textPresent, 'No text!'); - $this->assertTrue($htmlPresent, 'No HTML!'); - } - - /** - * @depends testGetSwiftMessage - */ - public function testSerialize() - { - $message = $this->createTestMessage(); - - $message->setTo($this->testEmailReceiver); - $message->setFrom('someuser@somedomain.com'); - $message->setSubject('Yii Swift Alternative Body Test'); - $message->setTextBody('Yii Swift test plain text body'); - - $serializedMessage = serialize($message); - $this->assertNotEmpty($serializedMessage, 'Unable to serialize message!'); - - $unserializedMessaage = unserialize($serializedMessage); - $this->assertEquals($message, $unserializedMessaage, 'Unable to unserialize message!'); - } - - /** - * @depends testSendAlternativeBody - */ - public function testAlternativeBodyCharset() - { - $message = $this->createTestMessage(); - $charset = 'windows-1251'; - $message->setCharset($charset); - - $message->setTextBody('some text'); - $message->setHtmlBody('some html'); - $content = $message->toString(); - $this->assertEquals(2, substr_count($content, $charset), 'Wrong charset for alternative body.'); - - $message->setTextBody('some text override'); - $content = $message->toString(); - $this->assertEquals(2, substr_count($content, $charset), 'Wrong charset for alternative body override.'); - } + /** + * @var string test email address, which will be used as receiver for the messages. + */ + protected $testEmailReceiver = 'someuser@somedomain.com'; + + public function setUp() + { + $this->mockApplication([ + 'components' => [ + 'mail' => $this->createTestEmailComponent() + ] + ]); + $filePath = $this->getTestFilePath(); + if (!file_exists($filePath)) { + FileHelper::createDirectory($filePath); + } + } + + public function tearDown() + { + $filePath = $this->getTestFilePath(); + if (file_exists($filePath)) { + FileHelper::removeDirectory($filePath); + } + } + + /** + * @return string test file path. + */ + protected function getTestFilePath() + { + return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid(); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer([ + 'useFileTransport' => true, + ]); + + return $component; + } + + /** + * @return Message test message instance. + */ + protected function createTestMessage() + { + return Yii::$app->getComponent('mail')->compose(); + } + + /** + * Creates image file with given text. + * @param string $fileName file name. + * @param string $text text to be applied on image. + * @return string image file full name. + */ + protected function createImageFile($fileName = 'test.jpg', $text = 'Test Image') + { + if (!function_exists('imagecreatetruecolor')) { + $this->markTestSkipped('GD lib required.'); + } + $fileFullName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $fileName; + $image = imagecreatetruecolor(120, 20); + $textColor = imagecolorallocate($image, 233, 14, 91); + imagestring($image, 1, 5, 5, $text, $textColor); + imagejpeg($image, $fileFullName); + imagedestroy($image); + + return $fileFullName; + } + + /** + * Finds the attachment object in the message. + * @param Message $message message instance + * @return null|\Swift_Mime_Attachment attachment instance. + */ + protected function getAttachment(Message $message) + { + $messageParts = $message->getSwiftMessage()->getChildren(); + $attachment = null; + foreach ($messageParts as $part) { + if ($part instanceof \Swift_Mime_Attachment) { + $attachment = $part; + break; + } + } + + return $attachment; + } + + // Tests : + + public function testGetSwiftMessage() + { + $message = new Message(); + $this->assertTrue(is_object($message->getSwiftMessage()), 'Unable to get Swift message!'); + } + + /** + * @depends testGetSwiftMessage + */ + public function testSetGet() + { + $message = new Message(); + + $charset = 'utf-16'; + $message->setCharset($charset); + $this->assertEquals($charset, $message->getCharset(), 'Unable to set charset!'); + + $subject = 'Test Subject'; + $message->setSubject($subject); + $this->assertEquals($subject, $message->getSubject(), 'Unable to set subject!'); + + $from = 'from@somedomain.com'; + $message->setFrom($from); + $this->assertContains($from, array_keys($message->getFrom()), 'Unable to set from!'); + + $replyTo = 'reply-to@somedomain.com'; + $message->setReplyTo($replyTo); + $this->assertContains($replyTo, array_keys($message->getReplyTo()), 'Unable to set replyTo!'); + + $to = 'someuser@somedomain.com'; + $message->setTo($to); + $this->assertContains($to, array_keys($message->getTo()), 'Unable to set to!'); + + $cc = 'ccuser@somedomain.com'; + $message->setCc($cc); + $this->assertContains($cc, array_keys($message->getCc()), 'Unable to set cc!'); + + $bcc = 'bccuser@somedomain.com'; + $message->setBcc($bcc); + $this->assertContains($bcc, array_keys($message->getBcc()), 'Unable to set bcc!'); + } + + /** + * @depends testGetSwiftMessage + */ + public function testSetupHeaders() + { + $charset = 'utf-16'; + $subject = 'Test Subject'; + $from = 'from@somedomain.com'; + $replyTo = 'reply-to@somedomain.com'; + $to = 'someuser@somedomain.com'; + $cc = 'ccuser@somedomain.com'; + $bcc = 'bccuser@somedomain.com'; + + $messageString = $this->createTestMessage() + ->setCharset($charset) + ->setSubject($subject) + ->setFrom($from) + ->setReplyTo($replyTo) + ->setTo($to) + ->setCc($cc) + ->setBcc($bcc) + ->toString(); + + $this->assertContains('charset=' . $charset, $messageString, 'Incorrect charset!'); + $this->assertContains('Subject: ' . $subject, $messageString, 'Incorrect "Subject" header!'); + $this->assertContains('From: ' . $from, $messageString, 'Incorrect "From" header!'); + $this->assertContains('Reply-To: ' . $replyTo, $messageString, 'Incorrect "Reply-To" header!'); + $this->assertContains('To: ' . $to, $messageString, 'Incorrect "To" header!'); + $this->assertContains('Cc: ' . $cc, $messageString, 'Incorrect "Cc" header!'); + $this->assertContains('Bcc: ' . $bcc, $messageString, 'Incorrect "Bcc" header!'); + } + + /** + * @depends testGetSwiftMessage + */ + public function testSend() + { + $message = $this->createTestMessage(); + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Test'); + $message->setTextBody('Yii Swift Test body'); + $this->assertTrue($message->send()); + } + + /** + * @depends testSend + */ + public function testAttachFile() + { + $message = $this->createTestMessage(); + + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Attach File Test'); + $message->setTextBody('Yii Swift Attach File Test body'); + $fileName = __FILE__; + $message->attach($fileName); + + $this->assertTrue($message->send()); + + $attachment = $this->getAttachment($message); + $this->assertTrue(is_object($attachment), 'No attachment found!'); + $this->assertContains($attachment->getFilename(), $fileName, 'Invalid file name!'); + } + + /** + * @depends testSend + */ + public function testAttachContent() + { + $message = $this->createTestMessage(); + + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Create Attachment Test'); + $message->setTextBody('Yii Swift Create Attachment Test body'); + $fileName = 'test.txt'; + $fileContent = 'Test attachment content'; + $message->attachContent($fileContent, ['fileName' => $fileName]); + + $this->assertTrue($message->send()); + + $attachment = $this->getAttachment($message); + $this->assertTrue(is_object($attachment), 'No attachment found!'); + $this->assertEquals($fileName, $attachment->getFilename(), 'Invalid file name!'); + } + + /** + * @depends testSend + */ + public function testEmbedFile() + { + $fileName = $this->createImageFile('embed_file.jpg', 'Embed Image File'); + + $message = $this->createTestMessage(); + + $cid = $message->embed($fileName); + + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Embed File Test'); + $message->setHtmlBody('Embed image: pic'); + + $this->assertTrue($message->send()); + + $attachment = $this->getAttachment($message); + $this->assertTrue(is_object($attachment), 'No attachment found!'); + $this->assertContains($attachment->getFilename(), $fileName, 'Invalid file name!'); + } + + /** + * @depends testSend + */ + public function testEmbedContent() + { + $fileFullName = $this->createImageFile('embed_file.jpg', 'Embed Image File'); + $message = $this->createTestMessage(); + + $fileName = basename($fileFullName); + $contentType = 'image/jpeg'; + $fileContent = file_get_contents($fileFullName); + + $cid = $message->embedContent($fileContent, ['fileName' => $fileName, 'contentType' => $contentType]); + + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Embed File Test'); + $message->setHtmlBody('Embed image: pic'); + + $this->assertTrue($message->send()); + + $attachment = $this->getAttachment($message); + $this->assertTrue(is_object($attachment), 'No attachment found!'); + $this->assertEquals($fileName, $attachment->getFilename(), 'Invalid file name!'); + $this->assertEquals($contentType, $attachment->getContentType(), 'Invalid content type!'); + } + + /** + * @depends testSend + */ + public function testSendAlternativeBody() + { + $message = $this->createTestMessage(); + + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Alternative Body Test'); + $message->setHtmlBody('Yii Swift test HTML body'); + $message->setTextBody('Yii Swift test plain text body'); + + $this->assertTrue($message->send()); + + $messageParts = $message->getSwiftMessage()->getChildren(); + $textPresent = false; + $htmlPresent = false; + foreach ($messageParts as $part) { + if (!($part instanceof \Swift_Mime_Attachment)) { + /* @var \Swift_Mime_MimePart $part */ + if ($part->getContentType() == 'text/plain') { + $textPresent = true; + } + if ($part->getContentType() == 'text/html') { + $htmlPresent = true; + } + } + } + $this->assertTrue($textPresent, 'No text!'); + $this->assertTrue($htmlPresent, 'No HTML!'); + } + + /** + * @depends testGetSwiftMessage + */ + public function testSerialize() + { + $message = $this->createTestMessage(); + + $message->setTo($this->testEmailReceiver); + $message->setFrom('someuser@somedomain.com'); + $message->setSubject('Yii Swift Alternative Body Test'); + $message->setTextBody('Yii Swift test plain text body'); + + $serializedMessage = serialize($message); + $this->assertNotEmpty($serializedMessage, 'Unable to serialize message!'); + + $unserializedMessaage = unserialize($serializedMessage); + $this->assertEquals($message, $unserializedMessaage, 'Unable to unserialize message!'); + } + + /** + * @depends testSendAlternativeBody + */ + public function testAlternativeBodyCharset() + { + $message = $this->createTestMessage(); + $charset = 'windows-1251'; + $message->setCharset($charset); + + $message->setTextBody('some text'); + $message->setHtmlBody('some html'); + $content = $message->toString(); + $this->assertEquals(2, substr_count($content, $charset), 'Wrong charset for alternative body.'); + + $message->setTextBody('some text override'); + $content = $message->toString(); + $this->assertEquals(2, substr_count($content, $charset), 'Wrong charset for alternative body override.'); + } } diff --git a/tests/unit/extensions/twig/ViewRendererTest.php b/tests/unit/extensions/twig/ViewRendererTest.php index 5e8440f49b9..4873425a017 100644 --- a/tests/unit/extensions/twig/ViewRendererTest.php +++ b/tests/unit/extensions/twig/ViewRendererTest.php @@ -1,13 +1,12 @@ */ namespace yiiunit\extensions\twig; - use yii\web\AssetManager; use yii\web\JqueryAsset; use yii\web\View; @@ -19,56 +18,57 @@ */ class ViewRendererTest extends TestCase { - protected function setUp() - { - $this->mockApplication(); - } + protected function setUp() + { + $this->mockApplication(); + } + + /** + * https://github.com/yiisoft/yii2/issues/1755 + */ + public function testLayoutAssets() + { + $view = $this->mockView(); + JqueryAsset::register($view); + $content = $view->renderFile('@yiiunit/extensions/twig/views/layout.twig'); - /** - * https://github.com/yiisoft/yii2/issues/1755 - */ - public function testLayoutAssets() - { - $view = $this->mockView(); - JqueryAsset::register($view); - $content = $view->renderFile('@yiiunit/extensions/twig/views/layout.twig'); + $this->assertEquals(1, preg_match('#\s*#', $content), 'content does not contain the jquery js:' . $content); + } - $this->assertEquals(1, preg_match('#\s*#', $content), 'content does not contain the jquery js:' . $content); - } + protected function mockView() + { + return new View([ + 'renderers' => [ + 'twig' => [ + 'class' => 'yii\twig\ViewRenderer', + //'cachePath' => '@runtime/Twig/cache', + 'options' => [ + 'cache' => false + ], + 'globals' => [ + 'html' => '\yii\helpers\Html', + 'pos_begin' => View::POS_BEGIN + ], + 'functions' => [ + 't' => '\Yii::t', + 'json_encode' => '\yii\helpers\Json::encode' + ] + ], + ], + 'assetManager' => $this->mockAssetManager(), + ]); + } - protected function mockView() - { - return new View([ - 'renderers' => [ - 'twig' => [ - 'class' => 'yii\twig\ViewRenderer', - //'cachePath' => '@runtime/Twig/cache', - 'options' => [ - 'cache' => false - ], - 'globals' => [ - 'html' => '\yii\helpers\Html', - 'pos_begin' => View::POS_BEGIN - ], - 'functions' => [ - 't' => '\Yii::t', - 'json_encode' => '\yii\helpers\Json::encode' - ] - ], - ], - 'assetManager' => $this->mockAssetManager(), - ]); - } + protected function mockAssetManager() + { + $assetDir = Yii::getAlias('@runtime/assets'); + if (!is_dir($assetDir)) { + mkdir($assetDir, 0777, true); + } - protected function mockAssetManager() - { - $assetDir = Yii::getAlias('@runtime/assets'); - if (!is_dir($assetDir)) { - mkdir($assetDir, 0777, true); - } - return new AssetManager([ - 'basePath' => $assetDir, - 'baseUrl' => '/assets', - ]); - } + return new AssetManager([ + 'basePath' => $assetDir, + 'baseUrl' => '/assets', + ]); + } } diff --git a/tests/unit/framework/BaseYiiTest.php b/tests/unit/framework/BaseYiiTest.php index 6011f6bbc67..ccc8d30b6cb 100644 --- a/tests/unit/framework/BaseYiiTest.php +++ b/tests/unit/framework/BaseYiiTest.php @@ -10,54 +10,54 @@ */ class BaseYiiTest extends TestCase { - public $aliases; - - protected function setUp() - { - parent::setUp(); - $this->aliases = Yii::$aliases; - } - - protected function tearDown() - { - parent::tearDown(); - Yii::$aliases = $this->aliases; - } - - public function testAlias() - { - $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); - - Yii::$aliases = []; - $this->assertFalse(Yii::getAlias('@yii', false)); - - Yii::setAlias('@yii', '/yii/framework'); - $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); - $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); - Yii::setAlias('@yii/gii', '/yii/gii'); - $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); - $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); - $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); - $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); - - Yii::setAlias('@tii', '@yii/test'); - $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); - - Yii::setAlias('@yii', null); - $this->assertFalse(Yii::getAlias('@yii', false)); - $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); - - Yii::setAlias('@some/alias', '/www'); - $this->assertEquals('/www', Yii::getAlias('@some/alias')); - } - - public function testGetVersion() - { - $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); - } - - public function testPowered() - { - $this->assertTrue(is_string(Yii::powered())); - } + public $aliases; + + protected function setUp() + { + parent::setUp(); + $this->aliases = Yii::$aliases; + } + + protected function tearDown() + { + parent::tearDown(); + Yii::$aliases = $this->aliases; + } + + public function testAlias() + { + $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); + + Yii::$aliases = []; + $this->assertFalse(Yii::getAlias('@yii', false)); + + Yii::setAlias('@yii', '/yii/framework'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + Yii::setAlias('@yii/gii', '/yii/gii'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@tii', '@yii/test'); + $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); + + Yii::setAlias('@yii', null); + $this->assertFalse(Yii::getAlias('@yii', false)); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@some/alias', '/www'); + $this->assertEquals('/www', Yii::getAlias('@some/alias')); + } + + public function testGetVersion() + { + $this->assertTrue((boolean) preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); + } + + public function testPowered() + { + $this->assertTrue(is_string(Yii::powered())); + } } diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php index e9010b6fffd..d162fb1d98f 100644 --- a/tests/unit/framework/ar/ActiveRecordTestTrait.php +++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php @@ -21,885 +21,884 @@ */ trait ActiveRecordTestTrait { - /** - * This method should call Customer::find($q) - * @param $q - * @return mixed - */ - public abstract function callCustomerFind($q = null); - - /** - * This method should call Order::find($q) - * @param $q - * @return mixed - */ - public abstract function callOrderFind($q = null); - - /** - * This method should call OrderItem::find($q) - * @param $q - * @return mixed - */ - public abstract function callOrderItemFind($q = null); - - /** - * This method should call Item::find($q) - * @param $q - * @return mixed - */ - public abstract function callItemFind($q = null); - - /** - * This method should return the classname of Customer class - * @return string - */ - public abstract function getCustomerClass(); - - /** - * This method should return the classname of Order class - * @return string - */ - public abstract function getOrderClass(); - - /** - * This method should return the classname of OrderItem class - * @return string - */ - public abstract function getOrderItemClass(); - - /** - * This method should return the classname of Item class - * @return string - */ - public abstract function getItemClass(); - - /** - * can be overridden to do things after save() - */ - public function afterSave() - { - } - - - public function testFind() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // find one - $result = $this->callCustomerFind(); - $this->assertTrue($result instanceof ActiveQueryInterface); - $customer = $result->one(); - $this->assertTrue($customer instanceof $customerClass); - - // find all - $customers = $this->callCustomerFind()->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers[0] instanceof $customerClass); - $this->assertTrue($customers[1] instanceof $customerClass); - $this->assertTrue($customers[2] instanceof $customerClass); - - // find all asArray - $customers = $this->callCustomerFind()->asArray()->all(); - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers[0]); - $this->assertArrayHasKey('name', $customers[0]); - $this->assertArrayHasKey('email', $customers[0]); - $this->assertArrayHasKey('address', $customers[0]); - $this->assertArrayHasKey('status', $customers[0]); - $this->assertArrayHasKey('id', $customers[1]); - $this->assertArrayHasKey('name', $customers[1]); - $this->assertArrayHasKey('email', $customers[1]); - $this->assertArrayHasKey('address', $customers[1]); - $this->assertArrayHasKey('status', $customers[1]); - $this->assertArrayHasKey('id', $customers[2]); - $this->assertArrayHasKey('name', $customers[2]); - $this->assertArrayHasKey('email', $customers[2]); - $this->assertArrayHasKey('address', $customers[2]); - $this->assertArrayHasKey('status', $customers[2]); - - // find by a single primary key - $customer = $this->callCustomerFind(2); - $this->assertTrue($customer instanceof $customerClass); - $this->assertEquals('user2', $customer->name); - $customer = $this->callCustomerFind(5); - $this->assertNull($customer); - $customer = $this->callCustomerFind(['id' => [5, 6, 1]]); - $this->assertEquals(1, count($customer)); - $customer = $this->callCustomerFind()->where(['id' => [5, 6, 1]])->one(); - $this->assertNotNull($customer); - - // find by column values - $customer = $this->callCustomerFind(['id' => 2, 'name' => 'user2']); - $this->assertTrue($customer instanceof $customerClass); - $this->assertEquals('user2', $customer->name); - $customer = $this->callCustomerFind(['id' => 2, 'name' => 'user1']); - $this->assertNull($customer); - $customer = $this->callCustomerFind(['id' => 5]); - $this->assertNull($customer); - $customer = $this->callCustomerFind(['name' => 'user5']); - $this->assertNull($customer); - - // find by attributes - $customer = $this->callCustomerFind()->where(['name' => 'user2'])->one(); - $this->assertTrue($customer instanceof $customerClass); - $this->assertEquals(2, $customer->id); - - // scope - $this->assertEquals(2, count($this->callCustomerFind()->active()->all())); - $this->assertEquals(2, $this->callCustomerFind()->active()->count()); - } - - public function testFindAsArray() - { - // asArray - $customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one(); - $this->assertEquals([ - 'id' => 2, - 'email' => 'user2@example.com', - 'name' => 'user2', - 'address' => 'address2', - 'status' => 1, - 'profile_id' => null, - ], $customer); - } - - public function testFindScalar() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - // query scalar - $customerName = $this->callCustomerFind()->where(['id' => 2])->scalar('name'); - $this->assertEquals('user2', $customerName); - $customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('name'); - $this->assertEquals('user3', $customerName); - $customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('noname'); - $this->assertNull($customerName); - $customerId = $this->callCustomerFind()->where(['status' => 2])->scalar('id'); - $this->assertEquals(3, $customerId); - } - - public function testFindColumn() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $this->assertEquals(['user1', 'user2', 'user3'], $this->callCustomerFind()->orderBy(['name' => SORT_ASC])->column('name')); - $this->assertEquals(['user3', 'user2', 'user1'], $this->callCustomerFind()->orderBy(['name' => SORT_DESC])->column('name')); - } - - public function testFindIndexBy() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // indexBy - $customers = $this->callCustomerFind()->indexBy('name')->orderBy('id')->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['user1'] instanceof $customerClass); - $this->assertTrue($customers['user2'] instanceof $customerClass); - $this->assertTrue($customers['user3'] instanceof $customerClass); - - // indexBy callable - $customers = $this->callCustomerFind()->indexBy(function ($customer) { - return $customer->id . '-' . $customer->name; - })->orderBy('id')->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers['1-user1'] instanceof $customerClass); - $this->assertTrue($customers['2-user2'] instanceof $customerClass); - $this->assertTrue($customers['3-user3'] instanceof $customerClass); - } - - public function testFindIndexByAsArray() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - // indexBy + asArray - $customers = $this->callCustomerFind()->asArray()->indexBy('name')->all(); - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers['user1']); - $this->assertArrayHasKey('name', $customers['user1']); - $this->assertArrayHasKey('email', $customers['user1']); - $this->assertArrayHasKey('address', $customers['user1']); - $this->assertArrayHasKey('status', $customers['user1']); - $this->assertArrayHasKey('id', $customers['user2']); - $this->assertArrayHasKey('name', $customers['user2']); - $this->assertArrayHasKey('email', $customers['user2']); - $this->assertArrayHasKey('address', $customers['user2']); - $this->assertArrayHasKey('status', $customers['user2']); - $this->assertArrayHasKey('id', $customers['user3']); - $this->assertArrayHasKey('name', $customers['user3']); - $this->assertArrayHasKey('email', $customers['user3']); - $this->assertArrayHasKey('address', $customers['user3']); - $this->assertArrayHasKey('status', $customers['user3']); - - // indexBy callable + asArray - $customers = $this->callCustomerFind()->indexBy(function ($customer) { - return $customer['id'] . '-' . $customer['name']; - })->asArray()->all(); - $this->assertEquals(3, count($customers)); - $this->assertArrayHasKey('id', $customers['1-user1']); - $this->assertArrayHasKey('name', $customers['1-user1']); - $this->assertArrayHasKey('email', $customers['1-user1']); - $this->assertArrayHasKey('address', $customers['1-user1']); - $this->assertArrayHasKey('status', $customers['1-user1']); - $this->assertArrayHasKey('id', $customers['2-user2']); - $this->assertArrayHasKey('name', $customers['2-user2']); - $this->assertArrayHasKey('email', $customers['2-user2']); - $this->assertArrayHasKey('address', $customers['2-user2']); - $this->assertArrayHasKey('status', $customers['2-user2']); - $this->assertArrayHasKey('id', $customers['3-user3']); - $this->assertArrayHasKey('name', $customers['3-user3']); - $this->assertArrayHasKey('email', $customers['3-user3']); - $this->assertArrayHasKey('address', $customers['3-user3']); - $this->assertArrayHasKey('status', $customers['3-user3']); - } - - public function testRefresh() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - $customer = new $customerClass(); - $this->assertFalse($customer->refresh()); - - $customer = $this->callCustomerFind(1); - $customer->name = 'to be refreshed'; - $this->assertTrue($customer->refresh()); - $this->assertEquals('user1', $customer->name); - } - - public function testEquals() - { - $customerClass = $this->getCustomerClass(); - $itemClass = $this->getItemClass(); - - /** @var TestCase|ActiveRecordTestTrait $this */ - $customerA = new $customerClass(); - $customerB = new $customerClass(); - $this->assertFalse($customerA->equals($customerB)); - - $customerA = new $customerClass(); - $customerB = new $itemClass(); - $this->assertFalse($customerA->equals($customerB)); - - $customerA = $this->callCustomerFind(1); - $customerB = $this->callCustomerFind(2); - $this->assertFalse($customerA->equals($customerB)); - - $customerB = $this->callCustomerFind(1); - $this->assertTrue($customerA->equals($customerB)); - - $customerA = $this->callCustomerFind(1); - $customerB = $this->callItemFind(1); - $this->assertFalse($customerA->equals($customerB)); - } - - public function testFindCount() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $this->assertEquals(3, $this->callCustomerFind()->count()); - - $this->assertEquals(1, $this->callCustomerFind()->where(['id' => 1])->count()); - $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->count()); - $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->offset(1)->count()); - $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->offset(2)->count()); - - // limit should have no effect on count() - $this->assertEquals(3, $this->callCustomerFind()->limit(1)->count()); - $this->assertEquals(3, $this->callCustomerFind()->limit(2)->count()); - $this->assertEquals(3, $this->callCustomerFind()->limit(10)->count()); - $this->assertEquals(3, $this->callCustomerFind()->offset(2)->limit(2)->count()); - } - - public function testFindLimit() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - // all() - $customers = $this->callCustomerFind()->all(); - $this->assertEquals(3, count($customers)); - - $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user1', $customers[0]->name); - - $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(1)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user2', $customers[0]->name); - - $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(2)->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals('user3', $customers[0]->name); - - $customers = $this->callCustomerFind()->orderBy('id')->limit(2)->offset(1)->all(); - $this->assertEquals(2, count($customers)); - $this->assertEquals('user2', $customers[0]->name); - $this->assertEquals('user3', $customers[1]->name); - - $customers = $this->callCustomerFind()->limit(2)->offset(3)->all(); - $this->assertEquals(0, count($customers)); - - // one() - $customer = $this->callCustomerFind()->orderBy('id')->one(); - $this->assertEquals('user1', $customer->name); - - $customer = $this->callCustomerFind()->orderBy('id')->offset(0)->one(); - $this->assertEquals('user1', $customer->name); - - $customer = $this->callCustomerFind()->orderBy('id')->offset(1)->one(); - $this->assertEquals('user2', $customer->name); - - $customer = $this->callCustomerFind()->orderBy('id')->offset(2)->one(); - $this->assertEquals('user3', $customer->name); - - $customer = $this->callCustomerFind()->offset(3)->one(); - $this->assertNull($customer); - - } - - public function testFindComplexCondition() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $this->assertEquals(2, $this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->count()); - $this->assertEquals(2, count($this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->all())); - - $this->assertEquals(2, $this->callCustomerFind()->where(['name' => ['user1', 'user2']])->count()); - $this->assertEquals(2, count($this->callCustomerFind()->where(['name' => ['user1', 'user2']])->all())); - - $this->assertEquals(1, $this->callCustomerFind()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->count()); - $this->assertEquals(1, count($this->callCustomerFind()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->all())); - } - - public function testFindNullValues() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $customer = $this->callCustomerFind(2); - $customer->name = null; - $customer->save(false); - $this->afterSave(); - - $result = $this->callCustomerFind()->where(['name' => null])->all(); - $this->assertEquals(1, count($result)); - $this->assertEquals(2, reset($result)->primaryKey); - } - - public function testExists() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $this->assertTrue($this->callCustomerFind()->where(['id' => 2])->exists()); - $this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists()); - $this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists()); - $this->assertFalse($this->callCustomerFind()->where(['name' => 'user5'])->exists()); - - $this->assertTrue($this->callCustomerFind()->where(['id' => [2, 3]])->exists()); - $this->assertTrue($this->callCustomerFind()->where(['id' => [2, 3]])->offset(1)->exists()); - $this->assertFalse($this->callCustomerFind()->where(['id' => [2, 3]])->offset(2)->exists()); - } - - public function testFindLazy() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $customer = $this->callCustomerFind(2); - $this->assertFalse($customer->isRelationPopulated('orders')); - $orders = $customer->orders; - $this->assertTrue($customer->isRelationPopulated('orders')); - $this->assertEquals(2, count($orders)); - $this->assertEquals(1, count($customer->relatedRecords)); - - // unset - unset($customer['orders']); - $this->assertFalse($customer->isRelationPopulated('orders')); - - /** @var Customer $customer */ - $customer = $this->callCustomerFind(2); - $this->assertFalse($customer->isRelationPopulated('orders')); - $orders = $customer->getOrders()->where(['id' => 3])->all(); - $this->assertFalse($customer->isRelationPopulated('orders')); - $this->assertEquals(0, count($customer->relatedRecords)); - - $this->assertEquals(1, count($orders)); - $this->assertEquals(3, $orders[0]->id); - } - - public function testFindEager() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $customers = $this->callCustomerFind()->with('orders')->indexBy('id')->all(); - ksort($customers); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers[1]->isRelationPopulated('orders')); - $this->assertTrue($customers[2]->isRelationPopulated('orders')); - $this->assertTrue($customers[3]->isRelationPopulated('orders')); - $this->assertEquals(1, count($customers[1]->orders)); - $this->assertEquals(2, count($customers[2]->orders)); - $this->assertEquals(0, count($customers[3]->orders)); - // unset - unset($customers[1]->orders); - $this->assertFalse($customers[1]->isRelationPopulated('orders')); - - $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one(); - $this->assertTrue($customer->isRelationPopulated('orders')); - $this->assertEquals(1, count($customer->orders)); - $this->assertEquals(1, count($customer->relatedRecords)); - - // multiple with() calls - $orders = $this->callOrderFind()->with('customer', 'items')->all(); - $this->assertEquals(3, count($orders)); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - $this->assertTrue($orders[0]->isRelationPopulated('items')); - $orders = $this->callOrderFind()->with('customer')->with('items')->all(); - $this->assertEquals(3, count($orders)); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - $this->assertTrue($orders[0]->isRelationPopulated('items')); - } - - public function testFindLazyVia() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - /** @var Order $order */ - $order = $this->callOrderFind(1); - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testFindLazyVia2() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - /** @var Order $order */ - $order = $this->callOrderFind(1); - $order->id = 100; - $this->assertEquals([], $order->items); - } - - public function testFindEagerViaRelation() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $orders = $this->callOrderFind()->with('items')->orderBy('id')->all(); - $this->assertEquals(3, count($orders)); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertTrue($order->isRelationPopulated('items')); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testFindNestedRelation() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $customers = $this->callCustomerFind()->with('orders', 'orders.items')->indexBy('id')->all(); - ksort($customers); - $this->assertEquals(3, count($customers)); - $this->assertTrue($customers[1]->isRelationPopulated('orders')); - $this->assertTrue($customers[2]->isRelationPopulated('orders')); - $this->assertTrue($customers[3]->isRelationPopulated('orders')); - $this->assertEquals(1, count($customers[1]->orders)); - $this->assertEquals(2, count($customers[2]->orders)); - $this->assertEquals(0, count($customers[3]->orders)); - $this->assertTrue($customers[1]->orders[0]->isRelationPopulated('items')); - $this->assertTrue($customers[2]->orders[0]->isRelationPopulated('items')); - $this->assertTrue($customers[2]->orders[1]->isRelationPopulated('items')); - $this->assertEquals(2, count($customers[1]->orders[0]->items)); - $this->assertEquals(3, count($customers[2]->orders[0]->items)); - $this->assertEquals(1, count($customers[2]->orders[1]->items)); - } - - /** - * Ensure ActiveRelationTrait does preserve order of items on find via() - * https://github.com/yiisoft/yii2/issues/1310 - */ - public function testFindEagerViaRelationPreserveOrder() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - - /* - Item (name, category_id) - Order (customer_id, created_at, total) - OrderItem (order_id, item_id, quantity, subtotal) - - Result should be the following: - - Order 1: 1, 1325282384, 110.0 - - orderItems: - OrderItem: 1, 1, 1, 30.0 - OrderItem: 1, 2, 2, 40.0 - - itemsInOrder: - Item 1: 'Agile Web Application Development with Yii1.1 and PHP5', 1 - Item 2: 'Yii 1.1 Application Development Cookbook', 1 - - Order 2: 2, 1325334482, 33.0 - - orderItems: - OrderItem: 2, 3, 1, 8.0 - OrderItem: 2, 4, 1, 10.0 - OrderItem: 2, 5, 1, 15.0 - - itemsInOrder: - Item 5: 'Cars', 2 - Item 3: 'Ice Age', 2 - Item 4: 'Toy Story', 2 - Order 3: 2, 1325502201, 40.0 - - orderItems: - OrderItem: 3, 2, 1, 40.0 - - itemsInOrder: - Item 3: 'Ice Age', 2 - */ - $orders = $this->callOrderFind()->with('itemsInOrder1')->orderBy('created_at')->all(); - $this->assertEquals(3, count($orders)); - - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); - $this->assertEquals(2, count($order->itemsInOrder1)); - $this->assertEquals(1, $order->itemsInOrder1[0]->id); - $this->assertEquals(2, $order->itemsInOrder1[1]->id); - - $order = $orders[1]; - $this->assertEquals(2, $order->id); - $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); - $this->assertEquals(3, count($order->itemsInOrder1)); - $this->assertEquals(5, $order->itemsInOrder1[0]->id); - $this->assertEquals(3, $order->itemsInOrder1[1]->id); - $this->assertEquals(4, $order->itemsInOrder1[2]->id); - - $order = $orders[2]; - $this->assertEquals(3, $order->id); - $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); - $this->assertEquals(1, count($order->itemsInOrder1)); - $this->assertEquals(2, $order->itemsInOrder1[0]->id); - } - - // different order in via table - public function testFindEagerViaRelationPreserveOrderB() - { - $orders = $this->callOrderFind()->with('itemsInOrder2')->orderBy('created_at')->all(); - $this->assertEquals(3, count($orders)); - - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); - $this->assertEquals(2, count($order->itemsInOrder2)); - $this->assertEquals(1, $order->itemsInOrder2[0]->id); - $this->assertEquals(2, $order->itemsInOrder2[1]->id); - - $order = $orders[1]; - $this->assertEquals(2, $order->id); - $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); - $this->assertEquals(3, count($order->itemsInOrder2)); - $this->assertEquals(5, $order->itemsInOrder2[0]->id); - $this->assertEquals(3, $order->itemsInOrder2[1]->id); - $this->assertEquals(4, $order->itemsInOrder2[2]->id); - - $order = $orders[2]; - $this->assertEquals(3, $order->id); - $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); - $this->assertEquals(1, count($order->itemsInOrder2)); - $this->assertEquals(2, $order->itemsInOrder2[0]->id); - } - - public function testLink() - { - $orderClass = $this->getOrderClass(); - $orderItemClass = $this->getOrderItemClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - $customer = $this->callCustomerFind(2); - $this->assertEquals(2, count($customer->orders)); - - // has many - $order = new $orderClass; - $order->total = 100; - $this->assertTrue($order->isNewRecord); - $customer->link('orders', $order); - $this->afterSave(); - $this->assertEquals(3, count($customer->orders)); - $this->assertFalse($order->isNewRecord); - $this->assertEquals(3, count($customer->getOrders()->all())); - $this->assertEquals(2, $order->customer_id); - - // belongs to - $order = new $orderClass; - $order->total = 100; - $this->assertTrue($order->isNewRecord); - $customer = $this->callCustomerFind(1); - $this->assertNull($order->customer); - $order->link('customer', $customer); - $this->assertFalse($order->isNewRecord); - $this->assertEquals(1, $order->customer_id); - $this->assertEquals(1, $order->customer->primaryKey); - - // via model - $order = $this->callOrderFind(1); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(2, count($order->orderItems)); - $orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]); - $this->assertNull($orderItem); - $item = $this->callItemFind(3); - $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]); - $this->afterSave(); - $this->assertEquals(3, count($order->items)); - $this->assertEquals(3, count($order->orderItems)); - $orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]); - $this->assertTrue($orderItem instanceof $orderItemClass); - $this->assertEquals(10, $orderItem->quantity); - $this->assertEquals(100, $orderItem->subtotal); - } - - public function testUnlink() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - // has many - $customer = $this->callCustomerFind(2); - $this->assertEquals(2, count($customer->orders)); - $customer->unlink('orders', $customer->orders[1], true); - $this->afterSave(); - $this->assertEquals(1, count($customer->orders)); - $this->assertNull($this->callOrderFind(3)); - - // via model - $order = $this->callOrderFind(2); - $this->assertEquals(3, count($order->items)); - $this->assertEquals(3, count($order->orderItems)); - $order->unlink('items', $order->items[2], true); - $this->afterSave(); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(2, count($order->orderItems)); - } - - public static $afterSaveNewRecord; - public static $afterSaveInsert; - - public function testInsert() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - $customer = new $customerClass; - $customer->email = 'user4@example.com'; - $customer->name = 'user4'; - $customer->address = 'address4'; - - $this->assertNull($customer->id); - $this->assertTrue($customer->isNewRecord); - static::$afterSaveNewRecord = null; - static::$afterSaveInsert = null; - - $customer->save(); - $this->afterSave(); - - $this->assertNotNull($customer->id); - $this->assertFalse(static::$afterSaveNewRecord); - $this->assertTrue(static::$afterSaveInsert); - $this->assertFalse($customer->isNewRecord); - } - - public function testUpdate() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // save - $customer = $this->callCustomerFind(2); - $this->assertTrue($customer instanceof $customerClass); - $this->assertEquals('user2', $customer->name); - $this->assertFalse($customer->isNewRecord); - static::$afterSaveNewRecord = null; - static::$afterSaveInsert = null; - - $customer->name = 'user2x'; - $customer->save(); - $this->afterSave(); - $this->assertEquals('user2x', $customer->name); - $this->assertFalse($customer->isNewRecord); - $this->assertFalse(static::$afterSaveNewRecord); - $this->assertFalse(static::$afterSaveInsert); - $customer2 = $this->callCustomerFind(2); - $this->assertEquals('user2x', $customer2->name); - - // updateAll - $customer = $this->callCustomerFind(3); - $this->assertEquals('user3', $customer->name); - $ret = $customerClass::updateAll(['name' => 'temp'], ['id' => 3]); - $this->afterSave(); - $this->assertEquals(1, $ret); - $customer = $this->callCustomerFind(3); - $this->assertEquals('temp', $customer->name); - - $ret = $customerClass::updateAll(['name' => 'tempX']); - $this->afterSave(); - $this->assertEquals(3, $ret); - - $ret = $customerClass::updateAll(['name' => 'temp'], ['name' => 'user6']); - $this->afterSave(); - $this->assertEquals(0, $ret); - } - - public function testUpdateAttributes() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // save - $customer = $this->callCustomerFind(2); - $this->assertTrue($customer instanceof $customerClass); - $this->assertEquals('user2', $customer->name); - $this->assertFalse($customer->isNewRecord); - static::$afterSaveNewRecord = null; - static::$afterSaveInsert = null; - - $customer->updateAttributes(['name' => 'user2x']); - $this->afterSave(); - $this->assertEquals('user2x', $customer->name); - $this->assertFalse($customer->isNewRecord); - $this->assertFalse(static::$afterSaveNewRecord); - $this->assertFalse(static::$afterSaveInsert); - $customer2 = $this->callCustomerFind(2); - $this->assertEquals('user2x', $customer2->name); - - $customer = $this->callCustomerFind(1); - $this->assertEquals('user1', $customer->name); - $this->assertEquals(1, $customer->status); - $customer->name = 'user1x'; - $customer->status = 2; - $customer->updateAttributes(['name']); - $this->assertEquals('user1x', $customer->name); - $this->assertEquals(2, $customer->status); - $customer = $this->callCustomerFind(1); - $this->assertEquals('user1x', $customer->name); - $this->assertEquals(1, $customer->status); - } - - public function testUpdateCounters() - { - $orderItemClass = $this->getOrderItemClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // updateCounters - $pk = ['order_id' => 2, 'item_id' => 4]; - $orderItem = $this->callOrderItemFind($pk); - $this->assertEquals(1, $orderItem->quantity); - $ret = $orderItem->updateCounters(['quantity' => -1]); - $this->afterSave(); - $this->assertEquals(1, $ret); - $this->assertEquals(0, $orderItem->quantity); - $orderItem = $this->callOrderItemFind($pk); - $this->assertEquals(0, $orderItem->quantity); - - // updateAllCounters - $pk = ['order_id' => 1, 'item_id' => 2]; - $orderItem = $this->callOrderItemFind($pk); - $this->assertEquals(2, $orderItem->quantity); - $ret = $orderItemClass::updateAllCounters([ - 'quantity' => 3, - 'subtotal' => -10, - ], $pk); - $this->afterSave(); - $this->assertEquals(1, $ret); - $orderItem = $this->callOrderItemFind($pk); - $this->assertEquals(5, $orderItem->quantity); - $this->assertEquals(30, $orderItem->subtotal); - } - - public function testDelete() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - // delete - $customer = $this->callCustomerFind(2); - $this->assertTrue($customer instanceof $customerClass); - $this->assertEquals('user2', $customer->name); - $customer->delete(); - $this->afterSave(); - $customer = $this->callCustomerFind(2); - $this->assertNull($customer); - - // deleteAll - $customers = $this->callCustomerFind()->all(); - $this->assertEquals(2, count($customers)); - $ret = $customerClass::deleteAll(); - $this->afterSave(); - $this->assertEquals(2, $ret); - $customers = $this->callCustomerFind()->all(); - $this->assertEquals(0, count($customers)); - - $ret = $customerClass::deleteAll(); - $this->afterSave(); - $this->assertEquals(0, $ret); - } - - /** - * Some PDO implementations(e.g. cubrid) do not support boolean values. - * Make sure this does not affect AR layer. - */ - public function testBooleanAttribute() - { - $customerClass = $this->getCustomerClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - $customer = new $customerClass(); - $customer->name = 'boolean customer'; - $customer->email = 'mail@example.com'; - $customer->status = true; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(1, $customer->status); - - $customer->status = false; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(0, $customer->status); - - $customers = $this->callCustomerFind()->where(['status' => true])->all(); - $this->assertEquals(2, count($customers)); - - $customers = $this->callCustomerFind()->where(['status' => false])->all(); - $this->assertEquals(1, count($customers)); - } - - public function testAfterFind() - { - /** @var BaseActiveRecord $customerClass */ - $customerClass = $this->getCustomerClass(); - /** @var BaseActiveRecord $orderClass */ - $orderClass = $this->getOrderClass(); - /** @var TestCase|ActiveRecordTestTrait $this */ - - $afterFindCalls = []; - Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) { - /** @var BaseActiveRecord $ar */ - $ar = $event->sender; - $afterFindCalls[] = [get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; - }); - - $customer = $this->callCustomerFind(1); - $this->assertNotNull($customer); - $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); - $afterFindCalls = []; - - $customer = $this->callCustomerFind()->where(['id' => 1])->one(); - $this->assertNotNull($customer); - $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); - $afterFindCalls = []; - - $customer = $this->callCustomerFind()->where(['id' => 1])->all(); - $this->assertNotNull($customer); - $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); - $afterFindCalls = []; - - $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->all(); - $this->assertNotNull($customer); - $this->assertEquals([ - [$this->getOrderClass(), false, 1, false], - [$customerClass, false, 1, true], - ], $afterFindCalls); - $afterFindCalls = []; - - if ($this instanceof \yiiunit\extensions\redis\ActiveRecordTest) { // TODO redis does not support orderBy() yet - $customer = $this->callCustomerFind()->where(['id' => [1, 2]])->with('orders')->all(); - } else { - // orderBy is needed to avoid random test failure - $customer = $this->callCustomerFind()->where(['id' => [1, 2]])->with('orders')->orderBy('name')->all(); - } - $this->assertNotNull($customer); - $this->assertEquals([ - [$orderClass, false, 1, false], - [$orderClass, false, 2, false], - [$orderClass, false, 3, false], - [$customerClass, false, 1, true], - [$customerClass, false, 2, true], - ], $afterFindCalls); - $afterFindCalls = []; - - Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND); - } + /** + * This method should call Customer::find($q) + * @param $q + * @return mixed + */ + abstract public function callCustomerFind($q = null); + + /** + * This method should call Order::find($q) + * @param $q + * @return mixed + */ + abstract public function callOrderFind($q = null); + + /** + * This method should call OrderItem::find($q) + * @param $q + * @return mixed + */ + abstract public function callOrderItemFind($q = null); + + /** + * This method should call Item::find($q) + * @param $q + * @return mixed + */ + abstract public function callItemFind($q = null); + + /** + * This method should return the classname of Customer class + * @return string + */ + abstract public function getCustomerClass(); + + /** + * This method should return the classname of Order class + * @return string + */ + abstract public function getOrderClass(); + + /** + * This method should return the classname of OrderItem class + * @return string + */ + abstract public function getOrderItemClass(); + + /** + * This method should return the classname of Item class + * @return string + */ + abstract public function getItemClass(); + + /** + * can be overridden to do things after save() + */ + public function afterSave() + { + } + + public function testFind() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // find one + $result = $this->callCustomerFind(); + $this->assertTrue($result instanceof ActiveQueryInterface); + $customer = $result->one(); + $this->assertTrue($customer instanceof $customerClass); + + // find all + $customers = $this->callCustomerFind()->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[0] instanceof $customerClass); + $this->assertTrue($customers[1] instanceof $customerClass); + $this->assertTrue($customers[2] instanceof $customerClass); + + // find all asArray + $customers = $this->callCustomerFind()->asArray()->all(); + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers[0]); + $this->assertArrayHasKey('name', $customers[0]); + $this->assertArrayHasKey('email', $customers[0]); + $this->assertArrayHasKey('address', $customers[0]); + $this->assertArrayHasKey('status', $customers[0]); + $this->assertArrayHasKey('id', $customers[1]); + $this->assertArrayHasKey('name', $customers[1]); + $this->assertArrayHasKey('email', $customers[1]); + $this->assertArrayHasKey('address', $customers[1]); + $this->assertArrayHasKey('status', $customers[1]); + $this->assertArrayHasKey('id', $customers[2]); + $this->assertArrayHasKey('name', $customers[2]); + $this->assertArrayHasKey('email', $customers[2]); + $this->assertArrayHasKey('address', $customers[2]); + $this->assertArrayHasKey('status', $customers[2]); + + // find by a single primary key + $customer = $this->callCustomerFind(2); + $this->assertTrue($customer instanceof $customerClass); + $this->assertEquals('user2', $customer->name); + $customer = $this->callCustomerFind(5); + $this->assertNull($customer); + $customer = $this->callCustomerFind(['id' => [5, 6, 1]]); + $this->assertEquals(1, count($customer)); + $customer = $this->callCustomerFind()->where(['id' => [5, 6, 1]])->one(); + $this->assertNotNull($customer); + + // find by column values + $customer = $this->callCustomerFind(['id' => 2, 'name' => 'user2']); + $this->assertTrue($customer instanceof $customerClass); + $this->assertEquals('user2', $customer->name); + $customer = $this->callCustomerFind(['id' => 2, 'name' => 'user1']); + $this->assertNull($customer); + $customer = $this->callCustomerFind(['id' => 5]); + $this->assertNull($customer); + $customer = $this->callCustomerFind(['name' => 'user5']); + $this->assertNull($customer); + + // find by attributes + $customer = $this->callCustomerFind()->where(['name' => 'user2'])->one(); + $this->assertTrue($customer instanceof $customerClass); + $this->assertEquals(2, $customer->id); + + // scope + $this->assertEquals(2, count($this->callCustomerFind()->active()->all())); + $this->assertEquals(2, $this->callCustomerFind()->active()->count()); + } + + public function testFindAsArray() + { + // asArray + $customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one(); + $this->assertEquals([ + 'id' => 2, + 'email' => 'user2@example.com', + 'name' => 'user2', + 'address' => 'address2', + 'status' => 1, + 'profile_id' => null, + ], $customer); + } + + public function testFindScalar() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + // query scalar + $customerName = $this->callCustomerFind()->where(['id' => 2])->scalar('name'); + $this->assertEquals('user2', $customerName); + $customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('name'); + $this->assertEquals('user3', $customerName); + $customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('noname'); + $this->assertNull($customerName); + $customerId = $this->callCustomerFind()->where(['status' => 2])->scalar('id'); + $this->assertEquals(3, $customerId); + } + + public function testFindColumn() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $this->assertEquals(['user1', 'user2', 'user3'], $this->callCustomerFind()->orderBy(['name' => SORT_ASC])->column('name')); + $this->assertEquals(['user3', 'user2', 'user1'], $this->callCustomerFind()->orderBy(['name' => SORT_DESC])->column('name')); + } + + public function testFindIndexBy() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // indexBy + $customers = $this->callCustomerFind()->indexBy('name')->orderBy('id')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['user1'] instanceof $customerClass); + $this->assertTrue($customers['user2'] instanceof $customerClass); + $this->assertTrue($customers['user3'] instanceof $customerClass); + + // indexBy callable + $customers = $this->callCustomerFind()->indexBy(function ($customer) { + return $customer->id . '-' . $customer->name; + })->orderBy('id')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['1-user1'] instanceof $customerClass); + $this->assertTrue($customers['2-user2'] instanceof $customerClass); + $this->assertTrue($customers['3-user3'] instanceof $customerClass); + } + + public function testFindIndexByAsArray() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + // indexBy + asArray + $customers = $this->callCustomerFind()->asArray()->indexBy('name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers['user1']); + $this->assertArrayHasKey('name', $customers['user1']); + $this->assertArrayHasKey('email', $customers['user1']); + $this->assertArrayHasKey('address', $customers['user1']); + $this->assertArrayHasKey('status', $customers['user1']); + $this->assertArrayHasKey('id', $customers['user2']); + $this->assertArrayHasKey('name', $customers['user2']); + $this->assertArrayHasKey('email', $customers['user2']); + $this->assertArrayHasKey('address', $customers['user2']); + $this->assertArrayHasKey('status', $customers['user2']); + $this->assertArrayHasKey('id', $customers['user3']); + $this->assertArrayHasKey('name', $customers['user3']); + $this->assertArrayHasKey('email', $customers['user3']); + $this->assertArrayHasKey('address', $customers['user3']); + $this->assertArrayHasKey('status', $customers['user3']); + + // indexBy callable + asArray + $customers = $this->callCustomerFind()->indexBy(function ($customer) { + return $customer['id'] . '-' . $customer['name']; + })->asArray()->all(); + $this->assertEquals(3, count($customers)); + $this->assertArrayHasKey('id', $customers['1-user1']); + $this->assertArrayHasKey('name', $customers['1-user1']); + $this->assertArrayHasKey('email', $customers['1-user1']); + $this->assertArrayHasKey('address', $customers['1-user1']); + $this->assertArrayHasKey('status', $customers['1-user1']); + $this->assertArrayHasKey('id', $customers['2-user2']); + $this->assertArrayHasKey('name', $customers['2-user2']); + $this->assertArrayHasKey('email', $customers['2-user2']); + $this->assertArrayHasKey('address', $customers['2-user2']); + $this->assertArrayHasKey('status', $customers['2-user2']); + $this->assertArrayHasKey('id', $customers['3-user3']); + $this->assertArrayHasKey('name', $customers['3-user3']); + $this->assertArrayHasKey('email', $customers['3-user3']); + $this->assertArrayHasKey('address', $customers['3-user3']); + $this->assertArrayHasKey('status', $customers['3-user3']); + } + + public function testRefresh() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + $customer = new $customerClass(); + $this->assertFalse($customer->refresh()); + + $customer = $this->callCustomerFind(1); + $customer->name = 'to be refreshed'; + $this->assertTrue($customer->refresh()); + $this->assertEquals('user1', $customer->name); + } + + public function testEquals() + { + $customerClass = $this->getCustomerClass(); + $itemClass = $this->getItemClass(); + + /** @var TestCase|ActiveRecordTestTrait $this */ + $customerA = new $customerClass(); + $customerB = new $customerClass(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = new $customerClass(); + $customerB = new $itemClass(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = $this->callCustomerFind(1); + $customerB = $this->callCustomerFind(2); + $this->assertFalse($customerA->equals($customerB)); + + $customerB = $this->callCustomerFind(1); + $this->assertTrue($customerA->equals($customerB)); + + $customerA = $this->callCustomerFind(1); + $customerB = $this->callItemFind(1); + $this->assertFalse($customerA->equals($customerB)); + } + + public function testFindCount() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $this->assertEquals(3, $this->callCustomerFind()->count()); + + $this->assertEquals(1, $this->callCustomerFind()->where(['id' => 1])->count()); + $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->count()); + $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->offset(1)->count()); + $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->offset(2)->count()); + + // limit should have no effect on count() + $this->assertEquals(3, $this->callCustomerFind()->limit(1)->count()); + $this->assertEquals(3, $this->callCustomerFind()->limit(2)->count()); + $this->assertEquals(3, $this->callCustomerFind()->limit(10)->count()); + $this->assertEquals(3, $this->callCustomerFind()->offset(2)->limit(2)->count()); + } + + public function testFindLimit() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + // all() + $customers = $this->callCustomerFind()->all(); + $this->assertEquals(3, count($customers)); + + $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user1', $customers[0]->name); + + $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(1)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user2', $customers[0]->name); + + $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(2)->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals('user3', $customers[0]->name); + + $customers = $this->callCustomerFind()->orderBy('id')->limit(2)->offset(1)->all(); + $this->assertEquals(2, count($customers)); + $this->assertEquals('user2', $customers[0]->name); + $this->assertEquals('user3', $customers[1]->name); + + $customers = $this->callCustomerFind()->limit(2)->offset(3)->all(); + $this->assertEquals(0, count($customers)); + + // one() + $customer = $this->callCustomerFind()->orderBy('id')->one(); + $this->assertEquals('user1', $customer->name); + + $customer = $this->callCustomerFind()->orderBy('id')->offset(0)->one(); + $this->assertEquals('user1', $customer->name); + + $customer = $this->callCustomerFind()->orderBy('id')->offset(1)->one(); + $this->assertEquals('user2', $customer->name); + + $customer = $this->callCustomerFind()->orderBy('id')->offset(2)->one(); + $this->assertEquals('user3', $customer->name); + + $customer = $this->callCustomerFind()->offset(3)->one(); + $this->assertNull($customer); + + } + + public function testFindComplexCondition() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $this->assertEquals(2, $this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->count()); + $this->assertEquals(2, count($this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->all())); + + $this->assertEquals(2, $this->callCustomerFind()->where(['name' => ['user1', 'user2']])->count()); + $this->assertEquals(2, count($this->callCustomerFind()->where(['name' => ['user1', 'user2']])->all())); + + $this->assertEquals(1, $this->callCustomerFind()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->count()); + $this->assertEquals(1, count($this->callCustomerFind()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->all())); + } + + public function testFindNullValues() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $customer = $this->callCustomerFind(2); + $customer->name = null; + $customer->save(false); + $this->afterSave(); + + $result = $this->callCustomerFind()->where(['name' => null])->all(); + $this->assertEquals(1, count($result)); + $this->assertEquals(2, reset($result)->primaryKey); + } + + public function testExists() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $this->assertTrue($this->callCustomerFind()->where(['id' => 2])->exists()); + $this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists()); + $this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists()); + $this->assertFalse($this->callCustomerFind()->where(['name' => 'user5'])->exists()); + + $this->assertTrue($this->callCustomerFind()->where(['id' => [2, 3]])->exists()); + $this->assertTrue($this->callCustomerFind()->where(['id' => [2, 3]])->offset(1)->exists()); + $this->assertFalse($this->callCustomerFind()->where(['id' => [2, 3]])->offset(2)->exists()); + } + + public function testFindLazy() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $customer = $this->callCustomerFind(2); + $this->assertFalse($customer->isRelationPopulated('orders')); + $orders = $customer->orders; + $this->assertTrue($customer->isRelationPopulated('orders')); + $this->assertEquals(2, count($orders)); + $this->assertEquals(1, count($customer->relatedRecords)); + + // unset + unset($customer['orders']); + $this->assertFalse($customer->isRelationPopulated('orders')); + + /** @var Customer $customer */ + $customer = $this->callCustomerFind(2); + $this->assertFalse($customer->isRelationPopulated('orders')); + $orders = $customer->getOrders()->where(['id' => 3])->all(); + $this->assertFalse($customer->isRelationPopulated('orders')); + $this->assertEquals(0, count($customer->relatedRecords)); + + $this->assertEquals(1, count($orders)); + $this->assertEquals(3, $orders[0]->id); + } + + public function testFindEager() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $customers = $this->callCustomerFind()->with('orders')->indexBy('id')->all(); + ksort($customers); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[1]->isRelationPopulated('orders')); + $this->assertTrue($customers[2]->isRelationPopulated('orders')); + $this->assertTrue($customers[3]->isRelationPopulated('orders')); + $this->assertEquals(1, count($customers[1]->orders)); + $this->assertEquals(2, count($customers[2]->orders)); + $this->assertEquals(0, count($customers[3]->orders)); + // unset + unset($customers[1]->orders); + $this->assertFalse($customers[1]->isRelationPopulated('orders')); + + $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one(); + $this->assertTrue($customer->isRelationPopulated('orders')); + $this->assertEquals(1, count($customer->orders)); + $this->assertEquals(1, count($customer->relatedRecords)); + + // multiple with() calls + $orders = $this->callOrderFind()->with('customer', 'items')->all(); + $this->assertEquals(3, count($orders)); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $orders = $this->callOrderFind()->with('customer')->with('items')->all(); + $this->assertEquals(3, count($orders)); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + } + + public function testFindLazyVia() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + /** @var Order $order */ + $order = $this->callOrderFind(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testFindLazyVia2() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + /** @var Order $order */ + $order = $this->callOrderFind(1); + $order->id = 100; + $this->assertEquals([], $order->items); + } + + public function testFindEagerViaRelation() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $orders = $this->callOrderFind()->with('items')->orderBy('id')->all(); + $this->assertEquals(3, count($orders)); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('items')); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testFindNestedRelation() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $customers = $this->callCustomerFind()->with('orders', 'orders.items')->indexBy('id')->all(); + ksort($customers); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[1]->isRelationPopulated('orders')); + $this->assertTrue($customers[2]->isRelationPopulated('orders')); + $this->assertTrue($customers[3]->isRelationPopulated('orders')); + $this->assertEquals(1, count($customers[1]->orders)); + $this->assertEquals(2, count($customers[2]->orders)); + $this->assertEquals(0, count($customers[3]->orders)); + $this->assertTrue($customers[1]->orders[0]->isRelationPopulated('items')); + $this->assertTrue($customers[2]->orders[0]->isRelationPopulated('items')); + $this->assertTrue($customers[2]->orders[1]->isRelationPopulated('items')); + $this->assertEquals(2, count($customers[1]->orders[0]->items)); + $this->assertEquals(3, count($customers[2]->orders[0]->items)); + $this->assertEquals(1, count($customers[2]->orders[1]->items)); + } + + /** + * Ensure ActiveRelationTrait does preserve order of items on find via() + * https://github.com/yiisoft/yii2/issues/1310 + */ + public function testFindEagerViaRelationPreserveOrder() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + + /* + Item (name, category_id) + Order (customer_id, created_at, total) + OrderItem (order_id, item_id, quantity, subtotal) + + Result should be the following: + + Order 1: 1, 1325282384, 110.0 + - orderItems: + OrderItem: 1, 1, 1, 30.0 + OrderItem: 1, 2, 2, 40.0 + - itemsInOrder: + Item 1: 'Agile Web Application Development with Yii1.1 and PHP5', 1 + Item 2: 'Yii 1.1 Application Development Cookbook', 1 + + Order 2: 2, 1325334482, 33.0 + - orderItems: + OrderItem: 2, 3, 1, 8.0 + OrderItem: 2, 4, 1, 10.0 + OrderItem: 2, 5, 1, 15.0 + - itemsInOrder: + Item 5: 'Cars', 2 + Item 3: 'Ice Age', 2 + Item 4: 'Toy Story', 2 + Order 3: 2, 1325502201, 40.0 + - orderItems: + OrderItem: 3, 2, 1, 40.0 + - itemsInOrder: + Item 3: 'Ice Age', 2 + */ + $orders = $this->callOrderFind()->with('itemsInOrder1')->orderBy('created_at')->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); + $this->assertEquals(2, count($order->itemsInOrder1)); + $this->assertEquals(1, $order->itemsInOrder1[0]->id); + $this->assertEquals(2, $order->itemsInOrder1[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); + $this->assertEquals(3, count($order->itemsInOrder1)); + $this->assertEquals(5, $order->itemsInOrder1[0]->id); + $this->assertEquals(3, $order->itemsInOrder1[1]->id); + $this->assertEquals(4, $order->itemsInOrder1[2]->id); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); + $this->assertEquals(1, count($order->itemsInOrder1)); + $this->assertEquals(2, $order->itemsInOrder1[0]->id); + } + + // different order in via table + public function testFindEagerViaRelationPreserveOrderB() + { + $orders = $this->callOrderFind()->with('itemsInOrder2')->orderBy('created_at')->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); + $this->assertEquals(2, count($order->itemsInOrder2)); + $this->assertEquals(1, $order->itemsInOrder2[0]->id); + $this->assertEquals(2, $order->itemsInOrder2[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); + $this->assertEquals(3, count($order->itemsInOrder2)); + $this->assertEquals(5, $order->itemsInOrder2[0]->id); + $this->assertEquals(3, $order->itemsInOrder2[1]->id); + $this->assertEquals(4, $order->itemsInOrder2[2]->id); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); + $this->assertEquals(1, count($order->itemsInOrder2)); + $this->assertEquals(2, $order->itemsInOrder2[0]->id); + } + + public function testLink() + { + $orderClass = $this->getOrderClass(); + $orderItemClass = $this->getOrderItemClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + $customer = $this->callCustomerFind(2); + $this->assertEquals(2, count($customer->orders)); + + // has many + $order = new $orderClass; + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer->link('orders', $order); + $this->afterSave(); + $this->assertEquals(3, count($customer->orders)); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(3, count($customer->getOrders()->all())); + $this->assertEquals(2, $order->customer_id); + + // belongs to + $order = new $orderClass; + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer = $this->callCustomerFind(1); + $this->assertNull($order->customer); + $order->link('customer', $customer); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(1, $order->customer_id); + $this->assertEquals(1, $order->customer->primaryKey); + + // via model + $order = $this->callOrderFind(1); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(2, count($order->orderItems)); + $orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]); + $this->assertNull($orderItem); + $item = $this->callItemFind(3); + $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]); + $this->afterSave(); + $this->assertEquals(3, count($order->items)); + $this->assertEquals(3, count($order->orderItems)); + $orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]); + $this->assertTrue($orderItem instanceof $orderItemClass); + $this->assertEquals(10, $orderItem->quantity); + $this->assertEquals(100, $orderItem->subtotal); + } + + public function testUnlink() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + // has many + $customer = $this->callCustomerFind(2); + $this->assertEquals(2, count($customer->orders)); + $customer->unlink('orders', $customer->orders[1], true); + $this->afterSave(); + $this->assertEquals(1, count($customer->orders)); + $this->assertNull($this->callOrderFind(3)); + + // via model + $order = $this->callOrderFind(2); + $this->assertEquals(3, count($order->items)); + $this->assertEquals(3, count($order->orderItems)); + $order->unlink('items', $order->items[2], true); + $this->afterSave(); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(2, count($order->orderItems)); + } + + public static $afterSaveNewRecord; + public static $afterSaveInsert; + + public function testInsert() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + $customer = new $customerClass; + $customer->email = 'user4@example.com'; + $customer->name = 'user4'; + $customer->address = 'address4'; + + $this->assertNull($customer->id); + $this->assertTrue($customer->isNewRecord); + static::$afterSaveNewRecord = null; + static::$afterSaveInsert = null; + + $customer->save(); + $this->afterSave(); + + $this->assertNotNull($customer->id); + $this->assertFalse(static::$afterSaveNewRecord); + $this->assertTrue(static::$afterSaveInsert); + $this->assertFalse($customer->isNewRecord); + } + + public function testUpdate() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // save + $customer = $this->callCustomerFind(2); + $this->assertTrue($customer instanceof $customerClass); + $this->assertEquals('user2', $customer->name); + $this->assertFalse($customer->isNewRecord); + static::$afterSaveNewRecord = null; + static::$afterSaveInsert = null; + + $customer->name = 'user2x'; + $customer->save(); + $this->afterSave(); + $this->assertEquals('user2x', $customer->name); + $this->assertFalse($customer->isNewRecord); + $this->assertFalse(static::$afterSaveNewRecord); + $this->assertFalse(static::$afterSaveInsert); + $customer2 = $this->callCustomerFind(2); + $this->assertEquals('user2x', $customer2->name); + + // updateAll + $customer = $this->callCustomerFind(3); + $this->assertEquals('user3', $customer->name); + $ret = $customerClass::updateAll(['name' => 'temp'], ['id' => 3]); + $this->afterSave(); + $this->assertEquals(1, $ret); + $customer = $this->callCustomerFind(3); + $this->assertEquals('temp', $customer->name); + + $ret = $customerClass::updateAll(['name' => 'tempX']); + $this->afterSave(); + $this->assertEquals(3, $ret); + + $ret = $customerClass::updateAll(['name' => 'temp'], ['name' => 'user6']); + $this->afterSave(); + $this->assertEquals(0, $ret); + } + + public function testUpdateAttributes() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // save + $customer = $this->callCustomerFind(2); + $this->assertTrue($customer instanceof $customerClass); + $this->assertEquals('user2', $customer->name); + $this->assertFalse($customer->isNewRecord); + static::$afterSaveNewRecord = null; + static::$afterSaveInsert = null; + + $customer->updateAttributes(['name' => 'user2x']); + $this->afterSave(); + $this->assertEquals('user2x', $customer->name); + $this->assertFalse($customer->isNewRecord); + $this->assertFalse(static::$afterSaveNewRecord); + $this->assertFalse(static::$afterSaveInsert); + $customer2 = $this->callCustomerFind(2); + $this->assertEquals('user2x', $customer2->name); + + $customer = $this->callCustomerFind(1); + $this->assertEquals('user1', $customer->name); + $this->assertEquals(1, $customer->status); + $customer->name = 'user1x'; + $customer->status = 2; + $customer->updateAttributes(['name']); + $this->assertEquals('user1x', $customer->name); + $this->assertEquals(2, $customer->status); + $customer = $this->callCustomerFind(1); + $this->assertEquals('user1x', $customer->name); + $this->assertEquals(1, $customer->status); + } + + public function testUpdateCounters() + { + $orderItemClass = $this->getOrderItemClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + $orderItem = $this->callOrderItemFind($pk); + $this->assertEquals(1, $orderItem->quantity); + $ret = $orderItem->updateCounters(['quantity' => -1]); + $this->afterSave(); + $this->assertEquals(1, $ret); + $this->assertEquals(0, $orderItem->quantity); + $orderItem = $this->callOrderItemFind($pk); + $this->assertEquals(0, $orderItem->quantity); + + // updateAllCounters + $pk = ['order_id' => 1, 'item_id' => 2]; + $orderItem = $this->callOrderItemFind($pk); + $this->assertEquals(2, $orderItem->quantity); + $ret = $orderItemClass::updateAllCounters([ + 'quantity' => 3, + 'subtotal' => -10, + ], $pk); + $this->afterSave(); + $this->assertEquals(1, $ret); + $orderItem = $this->callOrderItemFind($pk); + $this->assertEquals(5, $orderItem->quantity); + $this->assertEquals(30, $orderItem->subtotal); + } + + public function testDelete() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + // delete + $customer = $this->callCustomerFind(2); + $this->assertTrue($customer instanceof $customerClass); + $this->assertEquals('user2', $customer->name); + $customer->delete(); + $this->afterSave(); + $customer = $this->callCustomerFind(2); + $this->assertNull($customer); + + // deleteAll + $customers = $this->callCustomerFind()->all(); + $this->assertEquals(2, count($customers)); + $ret = $customerClass::deleteAll(); + $this->afterSave(); + $this->assertEquals(2, $ret); + $customers = $this->callCustomerFind()->all(); + $this->assertEquals(0, count($customers)); + + $ret = $customerClass::deleteAll(); + $this->afterSave(); + $this->assertEquals(0, $ret); + } + + /** + * Some PDO implementations(e.g. cubrid) do not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + $customerClass = $this->getCustomerClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + $customer = new $customerClass(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(1, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(0, $customer->status); + + $customers = $this->callCustomerFind()->where(['status' => true])->all(); + $this->assertEquals(2, count($customers)); + + $customers = $this->callCustomerFind()->where(['status' => false])->all(); + $this->assertEquals(1, count($customers)); + } + + public function testAfterFind() + { + /** @var BaseActiveRecord $customerClass */ + $customerClass = $this->getCustomerClass(); + /** @var BaseActiveRecord $orderClass */ + $orderClass = $this->getOrderClass(); + /** @var TestCase|ActiveRecordTestTrait $this */ + + $afterFindCalls = []; + Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) { + /** @var BaseActiveRecord $ar */ + $ar = $event->sender; + $afterFindCalls[] = [get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; + }); + + $customer = $this->callCustomerFind(1); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = $this->callCustomerFind()->where(['id' => 1])->one(); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = $this->callCustomerFind()->where(['id' => 1])->all(); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->all(); + $this->assertNotNull($customer); + $this->assertEquals([ + [$this->getOrderClass(), false, 1, false], + [$customerClass, false, 1, true], + ], $afterFindCalls); + $afterFindCalls = []; + + if ($this instanceof \yiiunit\extensions\redis\ActiveRecordTest) { // TODO redis does not support orderBy() yet + $customer = $this->callCustomerFind()->where(['id' => [1, 2]])->with('orders')->all(); + } else { + // orderBy is needed to avoid random test failure + $customer = $this->callCustomerFind()->where(['id' => [1, 2]])->with('orders')->orderBy('name')->all(); + } + $this->assertNotNull($customer); + $this->assertEquals([ + [$orderClass, false, 1, false], + [$orderClass, false, 2, false], + [$orderClass, false, 3, false], + [$customerClass, false, 1, true], + [$customerClass, false, 2, true], + ], $afterFindCalls); + $afterFindCalls = []; + + Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND); + } } diff --git a/tests/unit/framework/base/BehaviorTest.php b/tests/unit/framework/base/BehaviorTest.php index bfcbed0e27c..9a950f69901 100644 --- a/tests/unit/framework/base/BehaviorTest.php +++ b/tests/unit/framework/base/BehaviorTest.php @@ -12,38 +12,40 @@ class BarClass extends Component class FooClass extends Component { - public function behaviors() - { - return [ - 'foo' => __NAMESPACE__ . '\BarBehavior', - ]; - } + public function behaviors() + { + return [ + 'foo' => __NAMESPACE__ . '\BarBehavior', + ]; + } } class BarBehavior extends Behavior { - public $behaviorProperty = 'behavior property'; - - public function behaviorMethod() - { - return 'behavior method'; - } - - public function __call($name, $params) - { - if ($name == 'magicBehaviorMethod') { - return 'Magic Behavior Method Result!'; - } - return parent::__call($name, $params); - } - - public function hasMethod($name) - { - if ($name == 'magicBehaviorMethod') { - return true; - } - return parent::hasMethod($name); - } + public $behaviorProperty = 'behavior property'; + + public function behaviorMethod() + { + return 'behavior method'; + } + + public function __call($name, $params) + { + if ($name == 'magicBehaviorMethod') { + return 'Magic Behavior Method Result!'; + } + + return parent::__call($name, $params); + } + + public function hasMethod($name) + { + if ($name == 'magicBehaviorMethod') { + return true; + } + + return parent::hasMethod($name); + } } /** @@ -51,55 +53,55 @@ public function hasMethod($name) */ class BehaviorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - public function testAttachAndAccessing() - { - $bar = new BarClass(); - $behavior = new BarBehavior(); - $bar->attachBehavior('bar', $behavior); - $this->assertEquals('behavior property', $bar->behaviorProperty); - $this->assertEquals('behavior method', $bar->behaviorMethod()); - $this->assertEquals('behavior property', $bar->getBehavior('bar')->behaviorProperty); - $this->assertEquals('behavior method', $bar->getBehavior('bar')->behaviorMethod()); - - $behavior = new BarBehavior(['behaviorProperty' => 'reattached']); - $bar->attachBehavior('bar', $behavior); - $this->assertEquals('reattached', $bar->behaviorProperty); - } - - public function testAutomaticAttach() - { - $foo = new FooClass(); - $this->assertEquals('behavior property', $foo->behaviorProperty); - $this->assertEquals('behavior method', $foo->behaviorMethod()); - } - - public function testMagicMethods() - { - $bar = new BarClass(); - $behavior = new BarBehavior(); - - $this->assertFalse($bar->hasMethod('magicBehaviorMethod')); - $bar->attachBehavior('bar', $behavior); - $this->assertFalse($bar->hasMethod('magicBehaviorMethod', false)); - $this->assertTrue($bar->hasMethod('magicBehaviorMethod')); - - $this->assertEquals('Magic Behavior Method Result!', $bar->magicBehaviorMethod()); - } - - public function testCallUnknownMethod() - { - $bar = new BarClass(); - $behavior = new BarBehavior(); - $this->setExpectedException('yii\base\UnknownMethodException'); - - $this->assertFalse($bar->hasMethod('nomagicBehaviorMethod')); - $bar->attachBehavior('bar', $behavior); - $bar->nomagicBehaviorMethod(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testAttachAndAccessing() + { + $bar = new BarClass(); + $behavior = new BarBehavior(); + $bar->attachBehavior('bar', $behavior); + $this->assertEquals('behavior property', $bar->behaviorProperty); + $this->assertEquals('behavior method', $bar->behaviorMethod()); + $this->assertEquals('behavior property', $bar->getBehavior('bar')->behaviorProperty); + $this->assertEquals('behavior method', $bar->getBehavior('bar')->behaviorMethod()); + + $behavior = new BarBehavior(['behaviorProperty' => 'reattached']); + $bar->attachBehavior('bar', $behavior); + $this->assertEquals('reattached', $bar->behaviorProperty); + } + + public function testAutomaticAttach() + { + $foo = new FooClass(); + $this->assertEquals('behavior property', $foo->behaviorProperty); + $this->assertEquals('behavior method', $foo->behaviorMethod()); + } + + public function testMagicMethods() + { + $bar = new BarClass(); + $behavior = new BarBehavior(); + + $this->assertFalse($bar->hasMethod('magicBehaviorMethod')); + $bar->attachBehavior('bar', $behavior); + $this->assertFalse($bar->hasMethod('magicBehaviorMethod', false)); + $this->assertTrue($bar->hasMethod('magicBehaviorMethod')); + + $this->assertEquals('Magic Behavior Method Result!', $bar->magicBehaviorMethod()); + } + + public function testCallUnknownMethod() + { + $bar = new BarClass(); + $behavior = new BarBehavior(); + $this->setExpectedException('yii\base\UnknownMethodException'); + + $this->assertFalse($bar->hasMethod('nomagicBehaviorMethod')); + $bar->attachBehavior('bar', $behavior); + $bar->nomagicBehaviorMethod(); + } } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index d367cb50fbc..e95d387a3fb 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -8,13 +8,13 @@ function globalEventHandler($event) { - $event->sender->eventHandled = true; + $event->sender->eventHandled = true; } function globalEventHandler2($event) { - $event->sender->eventHandled = true; - $event->handled = true; + $event->sender->eventHandled = true; + $event->handled = true; } /** @@ -22,429 +22,430 @@ function globalEventHandler2($event) */ class ComponentTest extends TestCase { - /** - * @var NewComponent - */ - protected $component; - - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $this->component = new NewComponent(); - } - - protected function tearDown() - { - parent::tearDown(); - $this->component = null; - } - - public function testClone() - { - $component = new NewComponent(); - $behavior = new NewBehavior(); - $component->attachBehavior('a', $behavior); - $this->assertSame($behavior, $component->getBehavior('a')); - $component->on('test', 'fake'); - $this->assertTrue($component->hasEventHandlers('test')); - - $clone = clone $component; - $this->assertNotSame($component, $clone); - $this->assertNull($clone->getBehavior('a')); - $this->assertFalse($clone->hasEventHandlers('test')); - } - - public function testHasProperty() - { - $this->assertTrue($this->component->hasProperty('Text')); - $this->assertTrue($this->component->hasProperty('text')); - $this->assertFalse($this->component->hasProperty('Caption')); - $this->assertTrue($this->component->hasProperty('content')); - $this->assertFalse($this->component->hasProperty('content', false)); - $this->assertFalse($this->component->hasProperty('Content')); - } - - public function testCanGetProperty() - { - $this->assertTrue($this->component->canGetProperty('Text')); - $this->assertTrue($this->component->canGetProperty('text')); - $this->assertFalse($this->component->canGetProperty('Caption')); - $this->assertTrue($this->component->canGetProperty('content')); - $this->assertFalse($this->component->canGetProperty('content', false)); - $this->assertFalse($this->component->canGetProperty('Content')); - } - - public function testCanSetProperty() - { - $this->assertTrue($this->component->canSetProperty('Text')); - $this->assertTrue($this->component->canSetProperty('text')); - $this->assertFalse($this->component->canSetProperty('Object')); - $this->assertFalse($this->component->canSetProperty('Caption')); - $this->assertTrue($this->component->canSetProperty('content')); - $this->assertFalse($this->component->canSetProperty('content', false)); - $this->assertFalse($this->component->canSetProperty('Content')); - - // behavior - $this->assertFalse($this->component->canSetProperty('p2')); - $behavior = new NewBehavior(); - $this->component->attachBehavior('a', $behavior); - $this->assertTrue($this->component->canSetProperty('p2')); - $this->component->detachBehavior('a'); - } - - public function testGetProperty() - { - $this->assertTrue('default' === $this->component->Text); - $this->setExpectedException('yii\base\UnknownPropertyException'); - $value2 = $this->component->Caption; - } - - public function testSetProperty() - { - $value = 'new value'; - $this->component->Text = $value; - $this->assertEquals($value, $this->component->Text); - $this->setExpectedException('yii\base\UnknownPropertyException'); - $this->component->NewMember = $value; - } - - public function testIsset() - { - $this->assertTrue(isset($this->component->Text)); - $this->assertFalse(empty($this->component->Text)); - - $this->component->Text = ''; - $this->assertTrue(isset($this->component->Text)); - $this->assertTrue(empty($this->component->Text)); - - $this->component->Text = null; - $this->assertFalse(isset($this->component->Text)); - $this->assertTrue(empty($this->component->Text)); - - - $this->assertFalse(isset($this->component->p2)); - $this->component->attachBehavior('a', new NewBehavior()); - $this->component->setP2('test'); - $this->assertTrue(isset($this->component->p2)); - } - - public function testCallUnknownMethod() - { - $this->setExpectedException('yii\base\UnknownMethodException'); - $this->component->unknownMethod(); - } - - public function testUnset() - { - unset($this->component->Text); - $this->assertFalse(isset($this->component->Text)); - $this->assertTrue(empty($this->component->Text)); - - $this->component->attachBehavior('a', new NewBehavior()); - $this->component->setP2('test'); - $this->assertEquals('test', $this->component->getP2()); - - unset($this->component->p2); - $this->assertNull($this->component->getP2()); - } - - public function testUnsetReadonly() - { - $this->setExpectedException('yii\base\InvalidCallException'); - unset($this->component->object); - } - - public function testOn() - { - $this->assertFalse($this->component->hasEventHandlers('click')); - $this->component->on('click', 'foo'); - $this->assertTrue($this->component->hasEventHandlers('click')); - - $this->assertFalse($this->component->hasEventHandlers('click2')); - $p = 'on click2'; - $this->component->$p = 'foo2'; - $this->assertTrue($this->component->hasEventHandlers('click2')); - } - - public function testOff() - { - $this->assertFalse($this->component->hasEventHandlers('click')); - $this->component->on('click', 'foo'); - $this->assertTrue($this->component->hasEventHandlers('click')); - $this->component->off('click', 'foo'); - $this->assertFalse($this->component->hasEventHandlers('click')); - - $this->component->on('click2', 'foo'); - $this->component->on('click2', 'foo2'); - $this->component->on('click2', 'foo3'); - $this->assertTrue($this->component->hasEventHandlers('click2')); - $this->component->off('click2', 'foo3'); - $this->assertTrue($this->component->hasEventHandlers('click2')); - $this->component->off('click2'); - $this->assertFalse($this->component->hasEventHandlers('click2')); - } - - public function testTrigger() - { - $this->component->on('click', [$this->component, 'myEventHandler']); - $this->assertFalse($this->component->eventHandled); - $this->assertNull($this->component->event); - $this->component->raiseEvent(); - $this->assertTrue($this->component->eventHandled); - $this->assertEquals('click', $this->component->event->name); - $this->assertEquals($this->component, $this->component->event->sender); - $this->assertFalse($this->component->event->handled); - - $eventRaised = false; - $this->component->on('click', function ($event) use (&$eventRaised) { - $eventRaised = true; - }); - $this->component->raiseEvent(); - $this->assertTrue($eventRaised); - - // raise event w/o parameters - $eventRaised = false; - $this->component->on('test', function ($event) use (&$eventRaised) { - $eventRaised = true; - }); - $this->component->trigger('test'); - $this->assertTrue($eventRaised); - } - - public function testHasEventHandlers() - { - $this->assertFalse($this->component->hasEventHandlers('click')); - $this->component->on('click', 'foo'); - $this->assertTrue($this->component->hasEventHandlers('click')); - } - - public function testStopEvent() - { - $component = new NewComponent; - $component->on('click', 'yiiunit\framework\base\globalEventHandler2'); - $component->on('click', [$this->component, 'myEventHandler']); - $component->raiseEvent(); - $this->assertTrue($component->eventHandled); - $this->assertFalse($this->component->eventHandled); - } - - public function testAttachBehavior() - { - $component = new NewComponent; - $this->assertFalse($component->hasProperty('p')); - $this->assertFalse($component->behaviorCalled); - $this->assertNull($component->getBehavior('a')); - - $behavior = new NewBehavior; - $component->attachBehavior('a', $behavior); - $this->assertSame($behavior, $component->getBehavior('a')); - $this->assertTrue($component->hasProperty('p')); - $component->test(); - $this->assertTrue($component->behaviorCalled); - - $this->assertSame($behavior, $component->detachBehavior('a')); - $this->assertFalse($component->hasProperty('p')); - $this->setExpectedException('yii\base\UnknownMethodException'); - $component->test(); - - $p = 'as b'; - $component = new NewComponent; - $component->$p = ['class' => 'NewBehavior']; - $this->assertSame($behavior, $component->getBehavior('a')); - $this->assertTrue($component->hasProperty('p')); - $component->test(); - $this->assertTrue($component->behaviorCalled); - } - - public function testAttachBehaviors() - { - $component = new NewComponent; - $this->assertNull($component->getBehavior('a')); - $this->assertNull($component->getBehavior('b')); - - $behavior = new NewBehavior; - - $component->attachBehaviors([ - 'a' => $behavior, - 'b' => $behavior, - ]); - - $this->assertSame(['a' => $behavior, 'b' => $behavior], $component->getBehaviors()); - } - - public function testDetachBehavior() - { - $component = new NewComponent; - $behavior = new NewBehavior; - - $component->attachBehavior('a', $behavior); - $this->assertSame($behavior, $component->getBehavior('a')); - - $detachedBehavior = $component->detachBehavior('a'); - $this->assertSame($detachedBehavior, $behavior); - $this->assertNull($component->getBehavior('a')); - - $detachedBehavior = $component->detachBehavior('z'); - $this->assertNull($detachedBehavior); - } - - public function testDetachBehaviors() - { - $component = new NewComponent; - $behavior = new NewBehavior; - - $component->attachBehavior('a', $behavior); - $this->assertSame($behavior, $component->getBehavior('a')); - $component->attachBehavior('b', $behavior); - $this->assertSame($behavior, $component->getBehavior('b')); - - $component->detachBehaviors(); - $this->assertNull($component->getBehavior('a')); - $this->assertNull($component->getBehavior('b')); - } - - public function testSetReadOnlyProperty() - { - $this->setExpectedException( - '\yii\base\InvalidCallException', - 'Setting read-only property: yiiunit\framework\base\NewComponent::object' - ); - $this->component->object = 'z'; - } - - public function testSetPropertyOfBehavior() - { - $this->assertNull($this->component->getBehavior('a')); - - $behavior = new NewBehavior; - $this->component->attachBehaviors([ - 'a' => $behavior, - ]); - $this->component->p = 'Yii is cool.'; - - $this->assertSame('Yii is cool.', $this->component->getBehavior('a')->p); - } - - public function testSettingBehaviorWithSetter() - { - $behaviorName = 'foo'; - $this->assertNull($this->component->getBehavior($behaviorName)); - $p = 'as ' . $behaviorName; - $this->component->$p = __NAMESPACE__ . '\NewBehavior'; - $this->assertSame(__NAMESPACE__ . '\NewBehavior', get_class($this->component->getBehavior($behaviorName))); - } - - public function testWriteOnlyProperty() - { - $this->setExpectedException( - '\yii\base\InvalidCallException', - 'Getting write-only property: yiiunit\framework\base\NewComponent::writeOnly' - ); - $this->component->writeOnly; - } - - public function testSuccessfulMethodCheck() - { - $this->assertTrue($this->component->hasMethod('hasProperty')); - } - - public function testTurningOffNonExistingBehavior() - { - $this->assertFalse($this->component->hasEventHandlers('foo')); - $this->assertFalse($this->component->off('foo')); - } + /** + * @var NewComponent + */ + protected $component; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->component = new NewComponent(); + } + + protected function tearDown() + { + parent::tearDown(); + $this->component = null; + } + + public function testClone() + { + $component = new NewComponent(); + $behavior = new NewBehavior(); + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + $component->on('test', 'fake'); + $this->assertTrue($component->hasEventHandlers('test')); + + $clone = clone $component; + $this->assertNotSame($component, $clone); + $this->assertNull($clone->getBehavior('a')); + $this->assertFalse($clone->hasEventHandlers('test')); + } + + public function testHasProperty() + { + $this->assertTrue($this->component->hasProperty('Text')); + $this->assertTrue($this->component->hasProperty('text')); + $this->assertFalse($this->component->hasProperty('Caption')); + $this->assertTrue($this->component->hasProperty('content')); + $this->assertFalse($this->component->hasProperty('content', false)); + $this->assertFalse($this->component->hasProperty('Content')); + } + + public function testCanGetProperty() + { + $this->assertTrue($this->component->canGetProperty('Text')); + $this->assertTrue($this->component->canGetProperty('text')); + $this->assertFalse($this->component->canGetProperty('Caption')); + $this->assertTrue($this->component->canGetProperty('content')); + $this->assertFalse($this->component->canGetProperty('content', false)); + $this->assertFalse($this->component->canGetProperty('Content')); + } + + public function testCanSetProperty() + { + $this->assertTrue($this->component->canSetProperty('Text')); + $this->assertTrue($this->component->canSetProperty('text')); + $this->assertFalse($this->component->canSetProperty('Object')); + $this->assertFalse($this->component->canSetProperty('Caption')); + $this->assertTrue($this->component->canSetProperty('content')); + $this->assertFalse($this->component->canSetProperty('content', false)); + $this->assertFalse($this->component->canSetProperty('Content')); + + // behavior + $this->assertFalse($this->component->canSetProperty('p2')); + $behavior = new NewBehavior(); + $this->component->attachBehavior('a', $behavior); + $this->assertTrue($this->component->canSetProperty('p2')); + $this->component->detachBehavior('a'); + } + + public function testGetProperty() + { + $this->assertTrue('default' === $this->component->Text); + $this->setExpectedException('yii\base\UnknownPropertyException'); + $value2 = $this->component->Caption; + } + + public function testSetProperty() + { + $value = 'new value'; + $this->component->Text = $value; + $this->assertEquals($value, $this->component->Text); + $this->setExpectedException('yii\base\UnknownPropertyException'); + $this->component->NewMember = $value; + } + + public function testIsset() + { + $this->assertTrue(isset($this->component->Text)); + $this->assertFalse(empty($this->component->Text)); + + $this->component->Text = ''; + $this->assertTrue(isset($this->component->Text)); + $this->assertTrue(empty($this->component->Text)); + + $this->component->Text = null; + $this->assertFalse(isset($this->component->Text)); + $this->assertTrue(empty($this->component->Text)); + + $this->assertFalse(isset($this->component->p2)); + $this->component->attachBehavior('a', new NewBehavior()); + $this->component->setP2('test'); + $this->assertTrue(isset($this->component->p2)); + } + + public function testCallUnknownMethod() + { + $this->setExpectedException('yii\base\UnknownMethodException'); + $this->component->unknownMethod(); + } + + public function testUnset() + { + unset($this->component->Text); + $this->assertFalse(isset($this->component->Text)); + $this->assertTrue(empty($this->component->Text)); + + $this->component->attachBehavior('a', new NewBehavior()); + $this->component->setP2('test'); + $this->assertEquals('test', $this->component->getP2()); + + unset($this->component->p2); + $this->assertNull($this->component->getP2()); + } + + public function testUnsetReadonly() + { + $this->setExpectedException('yii\base\InvalidCallException'); + unset($this->component->object); + } + + public function testOn() + { + $this->assertFalse($this->component->hasEventHandlers('click')); + $this->component->on('click', 'foo'); + $this->assertTrue($this->component->hasEventHandlers('click')); + + $this->assertFalse($this->component->hasEventHandlers('click2')); + $p = 'on click2'; + $this->component->$p = 'foo2'; + $this->assertTrue($this->component->hasEventHandlers('click2')); + } + + public function testOff() + { + $this->assertFalse($this->component->hasEventHandlers('click')); + $this->component->on('click', 'foo'); + $this->assertTrue($this->component->hasEventHandlers('click')); + $this->component->off('click', 'foo'); + $this->assertFalse($this->component->hasEventHandlers('click')); + + $this->component->on('click2', 'foo'); + $this->component->on('click2', 'foo2'); + $this->component->on('click2', 'foo3'); + $this->assertTrue($this->component->hasEventHandlers('click2')); + $this->component->off('click2', 'foo3'); + $this->assertTrue($this->component->hasEventHandlers('click2')); + $this->component->off('click2'); + $this->assertFalse($this->component->hasEventHandlers('click2')); + } + + public function testTrigger() + { + $this->component->on('click', [$this->component, 'myEventHandler']); + $this->assertFalse($this->component->eventHandled); + $this->assertNull($this->component->event); + $this->component->raiseEvent(); + $this->assertTrue($this->component->eventHandled); + $this->assertEquals('click', $this->component->event->name); + $this->assertEquals($this->component, $this->component->event->sender); + $this->assertFalse($this->component->event->handled); + + $eventRaised = false; + $this->component->on('click', function ($event) use (&$eventRaised) { + $eventRaised = true; + }); + $this->component->raiseEvent(); + $this->assertTrue($eventRaised); + + // raise event w/o parameters + $eventRaised = false; + $this->component->on('test', function ($event) use (&$eventRaised) { + $eventRaised = true; + }); + $this->component->trigger('test'); + $this->assertTrue($eventRaised); + } + + public function testHasEventHandlers() + { + $this->assertFalse($this->component->hasEventHandlers('click')); + $this->component->on('click', 'foo'); + $this->assertTrue($this->component->hasEventHandlers('click')); + } + + public function testStopEvent() + { + $component = new NewComponent; + $component->on('click', 'yiiunit\framework\base\globalEventHandler2'); + $component->on('click', [$this->component, 'myEventHandler']); + $component->raiseEvent(); + $this->assertTrue($component->eventHandled); + $this->assertFalse($this->component->eventHandled); + } + + public function testAttachBehavior() + { + $component = new NewComponent; + $this->assertFalse($component->hasProperty('p')); + $this->assertFalse($component->behaviorCalled); + $this->assertNull($component->getBehavior('a')); + + $behavior = new NewBehavior; + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + $this->assertTrue($component->hasProperty('p')); + $component->test(); + $this->assertTrue($component->behaviorCalled); + + $this->assertSame($behavior, $component->detachBehavior('a')); + $this->assertFalse($component->hasProperty('p')); + $this->setExpectedException('yii\base\UnknownMethodException'); + $component->test(); + + $p = 'as b'; + $component = new NewComponent; + $component->$p = ['class' => 'NewBehavior']; + $this->assertSame($behavior, $component->getBehavior('a')); + $this->assertTrue($component->hasProperty('p')); + $component->test(); + $this->assertTrue($component->behaviorCalled); + } + + public function testAttachBehaviors() + { + $component = new NewComponent; + $this->assertNull($component->getBehavior('a')); + $this->assertNull($component->getBehavior('b')); + + $behavior = new NewBehavior; + + $component->attachBehaviors([ + 'a' => $behavior, + 'b' => $behavior, + ]); + + $this->assertSame(['a' => $behavior, 'b' => $behavior], $component->getBehaviors()); + } + + public function testDetachBehavior() + { + $component = new NewComponent; + $behavior = new NewBehavior; + + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + + $detachedBehavior = $component->detachBehavior('a'); + $this->assertSame($detachedBehavior, $behavior); + $this->assertNull($component->getBehavior('a')); + + $detachedBehavior = $component->detachBehavior('z'); + $this->assertNull($detachedBehavior); + } + + public function testDetachBehaviors() + { + $component = new NewComponent; + $behavior = new NewBehavior; + + $component->attachBehavior('a', $behavior); + $this->assertSame($behavior, $component->getBehavior('a')); + $component->attachBehavior('b', $behavior); + $this->assertSame($behavior, $component->getBehavior('b')); + + $component->detachBehaviors(); + $this->assertNull($component->getBehavior('a')); + $this->assertNull($component->getBehavior('b')); + } + + public function testSetReadOnlyProperty() + { + $this->setExpectedException( + '\yii\base\InvalidCallException', + 'Setting read-only property: yiiunit\framework\base\NewComponent::object' + ); + $this->component->object = 'z'; + } + + public function testSetPropertyOfBehavior() + { + $this->assertNull($this->component->getBehavior('a')); + + $behavior = new NewBehavior; + $this->component->attachBehaviors([ + 'a' => $behavior, + ]); + $this->component->p = 'Yii is cool.'; + + $this->assertSame('Yii is cool.', $this->component->getBehavior('a')->p); + } + + public function testSettingBehaviorWithSetter() + { + $behaviorName = 'foo'; + $this->assertNull($this->component->getBehavior($behaviorName)); + $p = 'as ' . $behaviorName; + $this->component->$p = __NAMESPACE__ . '\NewBehavior'; + $this->assertSame(__NAMESPACE__ . '\NewBehavior', get_class($this->component->getBehavior($behaviorName))); + } + + public function testWriteOnlyProperty() + { + $this->setExpectedException( + '\yii\base\InvalidCallException', + 'Getting write-only property: yiiunit\framework\base\NewComponent::writeOnly' + ); + $this->component->writeOnly; + } + + public function testSuccessfulMethodCheck() + { + $this->assertTrue($this->component->hasMethod('hasProperty')); + } + + public function testTurningOffNonExistingBehavior() + { + $this->assertFalse($this->component->hasEventHandlers('foo')); + $this->assertFalse($this->component->off('foo')); + } } class NewComponent extends Component { - private $_object = null; - private $_text = 'default'; - private $_items = []; - public $content; - - public function getText() - { - return $this->_text; - } - - public function setText($value) - { - $this->_text = $value; - } - - public function getObject() - { - if (!$this->_object) { - $this->_object = new self; - $this->_object->_text = 'object text'; - } - return $this->_object; - } - - public function getExecute() - { - return function ($param) { - return $param * 2; - }; - } - - public function getItems() - { - return $this->_items; - } - - public $eventHandled = false; - public $event; - public $behaviorCalled = false; - - public function myEventHandler($event) - { - $this->eventHandled = true; - $this->event = $event; - } - - public function raiseEvent() - { - $this->trigger('click', new Event); - } - - public function setWriteOnly() - { - } + private $_object = null; + private $_text = 'default'; + private $_items = []; + public $content; + + public function getText() + { + return $this->_text; + } + + public function setText($value) + { + $this->_text = $value; + } + + public function getObject() + { + if (!$this->_object) { + $this->_object = new self; + $this->_object->_text = 'object text'; + } + + return $this->_object; + } + + public function getExecute() + { + return function ($param) { + return $param * 2; + }; + } + + public function getItems() + { + return $this->_items; + } + + public $eventHandled = false; + public $event; + public $behaviorCalled = false; + + public function myEventHandler($event) + { + $this->eventHandled = true; + $this->event = $event; + } + + public function raiseEvent() + { + $this->trigger('click', new Event); + } + + public function setWriteOnly() + { + } } class NewBehavior extends Behavior { - public $p; - private $p2; - - public function getP2() - { - return $this->p2; - } - - public function setP2($value) - { - $this->p2 = $value; - } - - public function test() - { - $this->owner->behaviorCalled = true; - return 2; - } + public $p; + private $p2; + + public function getP2() + { + return $this->p2; + } + + public function setP2($value) + { + $this->p2 = $value; + } + + public function test() + { + $this->owner->behaviorCalled = true; + + return 2; + } } class NewComponent2 extends Component { - public $a; - public $b; - public $c; - - public function __construct($b, $c) - { - $this->b = $b; - $this->c = $c; - } + public $a; + public $b; + public $c; + + public function __construct($b, $c) + { + $this->b = $b; + $this->c = $c; + } } diff --git a/tests/unit/framework/base/DynamicModelTest.php b/tests/unit/framework/base/DynamicModelTest.php index 5468421b608..534f37215e6 100644 --- a/tests/unit/framework/base/DynamicModelTest.php +++ b/tests/unit/framework/base/DynamicModelTest.php @@ -17,64 +17,64 @@ */ class DynamicModelTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateData() - { - $email = 'invalid'; - $name = 'long name'; - $age = ''; - $model = DynamicModel::validateData(compact('name', 'email', 'age'), [ - [['email', 'name', 'age'], 'required'], - ['email', 'email'], - ['name', 'string', 'max' => 3], - ]); - $this->assertTrue($model->hasErrors()); - $this->assertTrue($model->hasErrors('email')); - $this->assertTrue($model->hasErrors('name')); - $this->assertTrue($model->hasErrors('age')); - } + public function testValidateData() + { + $email = 'invalid'; + $name = 'long name'; + $age = ''; + $model = DynamicModel::validateData(compact('name', 'email', 'age'), [ + [['email', 'name', 'age'], 'required'], + ['email', 'email'], + ['name', 'string', 'max' => 3], + ]); + $this->assertTrue($model->hasErrors()); + $this->assertTrue($model->hasErrors('email')); + $this->assertTrue($model->hasErrors('name')); + $this->assertTrue($model->hasErrors('age')); + } - public function testAddRule() - { - $model = new DynamicModel(); - $this->assertEquals(0, $model->getValidators()->count()); - $model->addRule('name', 'string', ['min' => 12]); - $this->assertEquals(1, $model->getValidators()->count()); - $model->addRule('email', 'email'); - $this->assertEquals(2, $model->getValidators()->count()); - $model->addRule(['name', 'email'], 'required'); - $this->assertEquals(3, $model->getValidators()->count()); - } + public function testAddRule() + { + $model = new DynamicModel(); + $this->assertEquals(0, $model->getValidators()->count()); + $model->addRule('name', 'string', ['min' => 12]); + $this->assertEquals(1, $model->getValidators()->count()); + $model->addRule('email', 'email'); + $this->assertEquals(2, $model->getValidators()->count()); + $model->addRule(['name', 'email'], 'required'); + $this->assertEquals(3, $model->getValidators()->count()); + } - public function testValidateWithAddRule() - { - $email = 'invalid'; - $name = 'long name'; - $age = ''; - $model = new DynamicModel(compact('name', 'email', 'age')); - $model->addRule(['email', 'name', 'age'], 'required') - ->addRule('email', 'email') - ->addRule('name', 'string', ['max' => 3]) - ->validate(); - $this->assertTrue($model->hasErrors()); - $this->assertTrue($model->hasErrors('email')); - $this->assertTrue($model->hasErrors('name')); - $this->assertTrue($model->hasErrors('age')); - } + public function testValidateWithAddRule() + { + $email = 'invalid'; + $name = 'long name'; + $age = ''; + $model = new DynamicModel(compact('name', 'email', 'age')); + $model->addRule(['email', 'name', 'age'], 'required') + ->addRule('email', 'email') + ->addRule('name', 'string', ['max' => 3]) + ->validate(); + $this->assertTrue($model->hasErrors()); + $this->assertTrue($model->hasErrors('email')); + $this->assertTrue($model->hasErrors('name')); + $this->assertTrue($model->hasErrors('age')); + } - public function testDynamicProperty() - { - $email = 'invalid'; - $name = 'long name'; - $model = new DynamicModel(compact('name', 'email')); - $this->assertEquals($email, $model->email); - $this->assertEquals($name, $model->name); - $this->setExpectedException('yii\base\UnknownPropertyException'); - $age = $model->age; - } + public function testDynamicProperty() + { + $email = 'invalid'; + $name = 'long name'; + $model = new DynamicModel(compact('name', 'email')); + $this->assertEquals($email, $model->email); + $this->assertEquals($name, $model->name); + $this->setExpectedException('yii\base\UnknownPropertyException'); + $age = $model->age; + } } diff --git a/tests/unit/framework/base/EventTest.php b/tests/unit/framework/base/EventTest.php index 947e4545fe5..8fda502dd27 100644 --- a/tests/unit/framework/base/EventTest.php +++ b/tests/unit/framework/base/EventTest.php @@ -17,70 +17,70 @@ */ class EventTest extends TestCase { - public $counter; + public $counter; - public function setUp() - { - $this->counter = 0; - Event::off(ActiveRecord::className(), 'save'); - Event::off(Post::className(), 'save'); - Event::off(User::className(), 'save'); - } + public function setUp() + { + $this->counter = 0; + Event::off(ActiveRecord::className(), 'save'); + Event::off(Post::className(), 'save'); + Event::off(User::className(), 'save'); + } - public function testOn() - { - Event::on(Post::className(), 'save', function ($event) { - $this->counter += 1; - }); - Event::on(ActiveRecord::className(), 'save', function ($event) { - $this->counter += 3; - }); - $this->assertEquals(0, $this->counter); - $post = new Post; - $post->save(); - $this->assertEquals(4, $this->counter); - $user = new User; - $user->save(); - $this->assertEquals(7, $this->counter); - } + public function testOn() + { + Event::on(Post::className(), 'save', function ($event) { + $this->counter += 1; + }); + Event::on(ActiveRecord::className(), 'save', function ($event) { + $this->counter += 3; + }); + $this->assertEquals(0, $this->counter); + $post = new Post; + $post->save(); + $this->assertEquals(4, $this->counter); + $user = new User; + $user->save(); + $this->assertEquals(7, $this->counter); + } - public function testOff() - { - $handler = function ($event) { - $this->counter ++; - }; - $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); - Event::on(Post::className(), 'save', $handler); - $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); - Event::off(Post::className(), 'save', $handler); - $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); - } + public function testOff() + { + $handler = function ($event) { + $this->counter ++; + }; + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + Event::on(Post::className(), 'save', $handler); + $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); + Event::off(Post::className(), 'save', $handler); + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + } - public function testHasHandlers() - { - $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); - $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); - Event::on(Post::className(), 'save', function ($event) { - $this->counter += 1; - }); - $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); - $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + public function testHasHandlers() + { + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + Event::on(Post::className(), 'save', function ($event) { + $this->counter += 1; + }); + $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); + $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); - $this->assertFalse(Event::hasHandlers(User::className(), 'save')); - Event::on(ActiveRecord::className(), 'save', function ($event) { - $this->counter += 1; - }); - $this->assertTrue(Event::hasHandlers(User::className(), 'save')); - $this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save')); - } + $this->assertFalse(Event::hasHandlers(User::className(), 'save')); + Event::on(ActiveRecord::className(), 'save', function ($event) { + $this->counter += 1; + }); + $this->assertTrue(Event::hasHandlers(User::className(), 'save')); + $this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save')); + } } class ActiveRecord extends Component { - public function save() - { - $this->trigger('save'); - } + public function save() + { + $this->trigger('save'); + } } class Post extends ActiveRecord diff --git a/tests/unit/framework/base/FormatterTest.php b/tests/unit/framework/base/FormatterTest.php index 322b751a11a..a7e49b28f00 100644 --- a/tests/unit/framework/base/FormatterTest.php +++ b/tests/unit/framework/base/FormatterTest.php @@ -14,187 +14,187 @@ */ class FormatterTest extends TestCase { - /** - * @var Formatter - */ - protected $formatter; - - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $this->formatter = new Formatter(); - } - - protected function tearDown() - { - parent::tearDown(); - $this->formatter = null; - } - - public function testAsRaw() - { - $value = '123'; - $this->assertSame($value, $this->formatter->asRaw($value)); - $value = 123; - $this->assertSame($value, $this->formatter->asRaw($value)); - $value = '<>'; - $this->assertSame($value, $this->formatter->asRaw($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asRaw(null)); - } - - public function testAsText() - { - $value = '123'; - $this->assertSame($value, $this->formatter->asText($value)); - $value = 123; - $this->assertSame("$value", $this->formatter->asText($value)); - $value = '<>'; - $this->assertSame('<>', $this->formatter->asText($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asText(null)); - } - - public function testAsNtext() - { - $value = '123'; - $this->assertSame($value, $this->formatter->asNtext($value)); - $value = 123; - $this->assertSame("$value", $this->formatter->asNtext($value)); - $value = '<>'; - $this->assertSame('<>', $this->formatter->asNtext($value)); - $value = "123\n456"; - $this->assertSame("123
        \n456", $this->formatter->asNtext($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNtext(null)); - } - - public function testAsParagraphs() - { - $value = '123'; - $this->assertSame("

        $value

        ", $this->formatter->asParagraphs($value)); - $value = 123; - $this->assertSame("

        $value

        ", $this->formatter->asParagraphs($value)); - $value = '<>'; - $this->assertSame('

        <>

        ', $this->formatter->asParagraphs($value)); - $value = "123\n456"; - $this->assertSame("

        123\n456

        ", $this->formatter->asParagraphs($value)); - $value = "123\n\n456"; - $this->assertSame("

        123

        \n

        456

        ", $this->formatter->asParagraphs($value)); - $value = "123\n\n\n456"; - $this->assertSame("

        123

        \n

        456

        ", $this->formatter->asParagraphs($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asParagraphs(null)); - } - - public function testAsHtml() - { - // todo: dependency on HtmlPurifier - } - - public function testAsEmail() - { - $value = 'test@sample.com'; - $this->assertSame("$value", $this->formatter->asEmail($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asEmail(null)); - } - - public function testAsImage() - { - $value = 'http://sample.com/img.jpg'; - $this->assertSame("\"\"", $this->formatter->asImage($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asImage(null)); - } - - public function testAsBoolean() - { - $value = true; - $this->assertSame('Yes', $this->formatter->asBoolean($value)); - $value = false; - $this->assertSame('No', $this->formatter->asBoolean($value)); - $value = "111"; - $this->assertSame('Yes', $this->formatter->asBoolean($value)); - $value = ""; - $this->assertSame('No', $this->formatter->asBoolean($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asBoolean(null)); - } - - public function testAsDate() - { - $value = time(); - $this->assertSame(date('Y/m/d', $value), $this->formatter->asDate($value)); - $this->assertSame(date('Y-m-d', $value), $this->formatter->asDate($value, 'Y-m-d')); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); - } - - public function testAsTime() - { - $value = time(); - $this->assertSame(date('h:i:s A', $value), $this->formatter->asTime($value)); - $this->assertSame(date('h:i:s', $value), $this->formatter->asTime($value, 'h:i:s')); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asTime(null)); - } - - public function testAsDatetime() - { - $value = time(); - $this->assertSame(date('Y/m/d h:i:s A', $value), $this->formatter->asDatetime($value)); - $this->assertSame(date('Y-m-d h:i:s', $value), $this->formatter->asDatetime($value, 'Y-m-d h:i:s')); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); - } - - public function testAsInteger() - { - $value = 123; - $this->assertSame("$value", $this->formatter->asInteger($value)); - $value = 123.23; - $this->assertSame("123", $this->formatter->asInteger($value)); - $value = 'a'; - $this->assertSame("0", $this->formatter->asInteger($value)); - $value = -123.23; - $this->assertSame("-123", $this->formatter->asInteger($value)); - $value = "-123abc"; - $this->assertSame("-123", $this->formatter->asInteger($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null)); - } - - public function testAsDouble() - { - $value = 123.12; - $this->assertSame("123.12", $this->formatter->asDouble($value)); - $this->assertSame("123.1", $this->formatter->asDouble($value, 1)); - $this->assertSame("123", $this->formatter->asDouble($value, 0)); - $value = 123; - $this->assertSame("123.00", $this->formatter->asDouble($value)); - $this->formatter->decimalSeparator = ','; - $value = 123.12; - $this->assertSame("123,12", $this->formatter->asDouble($value)); - $this->assertSame("123,1", $this->formatter->asDouble($value, 1)); - $this->assertSame("123", $this->formatter->asDouble($value, 0)); - $value = 123123.123; - $this->assertSame("123123,12", $this->formatter->asDouble($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDouble(null)); - } - - public function testAsNumber() - { - $value = 123123.123; - $this->assertSame("123,123", $this->formatter->asNumber($value)); - $this->assertSame("123,123.12", $this->formatter->asNumber($value, 2)); - $this->formatter->decimalSeparator = ','; - $this->formatter->thousandSeparator = ' '; - $this->assertSame("123 123", $this->formatter->asNumber($value)); - $this->assertSame("123 123,12", $this->formatter->asNumber($value, 2)); - $this->formatter->thousandSeparator = ''; - $this->assertSame("123123", $this->formatter->asNumber($value)); - $this->assertSame("123123,12", $this->formatter->asNumber($value, 2)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNumber(null)); - } - - public function testFormat() - { - $value = time(); - $this->assertSame(date('Y/m/d', $value), $this->formatter->format($value, 'date')); - $this->assertSame(date('Y/m/d', $value), $this->formatter->format($value, 'DATE')); - $this->assertSame(date('Y-m-d', $value), $this->formatter->format($value, ['date', 'Y-m-d'])); - $this->setExpectedException('\yii\base\InvalidParamException'); - $this->assertSame(date('Y-m-d', $value), $this->formatter->format($value, 'data')); - } + /** + * @var Formatter + */ + protected $formatter; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->formatter = new Formatter(); + } + + protected function tearDown() + { + parent::tearDown(); + $this->formatter = null; + } + + public function testAsRaw() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asRaw($value)); + $value = 123; + $this->assertSame($value, $this->formatter->asRaw($value)); + $value = '<>'; + $this->assertSame($value, $this->formatter->asRaw($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asRaw(null)); + } + + public function testAsText() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asText($value)); + $value = 123; + $this->assertSame("$value", $this->formatter->asText($value)); + $value = '<>'; + $this->assertSame('<>', $this->formatter->asText($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asText(null)); + } + + public function testAsNtext() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asNtext($value)); + $value = 123; + $this->assertSame("$value", $this->formatter->asNtext($value)); + $value = '<>'; + $this->assertSame('<>', $this->formatter->asNtext($value)); + $value = "123\n456"; + $this->assertSame("123
        \n456", $this->formatter->asNtext($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNtext(null)); + } + + public function testAsParagraphs() + { + $value = '123'; + $this->assertSame("

        $value

        ", $this->formatter->asParagraphs($value)); + $value = 123; + $this->assertSame("

        $value

        ", $this->formatter->asParagraphs($value)); + $value = '<>'; + $this->assertSame('

        <>

        ', $this->formatter->asParagraphs($value)); + $value = "123\n456"; + $this->assertSame("

        123\n456

        ", $this->formatter->asParagraphs($value)); + $value = "123\n\n456"; + $this->assertSame("

        123

        \n

        456

        ", $this->formatter->asParagraphs($value)); + $value = "123\n\n\n456"; + $this->assertSame("

        123

        \n

        456

        ", $this->formatter->asParagraphs($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asParagraphs(null)); + } + + public function testAsHtml() + { + // todo: dependency on HtmlPurifier + } + + public function testAsEmail() + { + $value = 'test@sample.com'; + $this->assertSame("$value", $this->formatter->asEmail($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asEmail(null)); + } + + public function testAsImage() + { + $value = 'http://sample.com/img.jpg'; + $this->assertSame("\"\"", $this->formatter->asImage($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asImage(null)); + } + + public function testAsBoolean() + { + $value = true; + $this->assertSame('Yes', $this->formatter->asBoolean($value)); + $value = false; + $this->assertSame('No', $this->formatter->asBoolean($value)); + $value = "111"; + $this->assertSame('Yes', $this->formatter->asBoolean($value)); + $value = ""; + $this->assertSame('No', $this->formatter->asBoolean($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asBoolean(null)); + } + + public function testAsDate() + { + $value = time(); + $this->assertSame(date('Y/m/d', $value), $this->formatter->asDate($value)); + $this->assertSame(date('Y-m-d', $value), $this->formatter->asDate($value, 'Y-m-d')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); + } + + public function testAsTime() + { + $value = time(); + $this->assertSame(date('h:i:s A', $value), $this->formatter->asTime($value)); + $this->assertSame(date('h:i:s', $value), $this->formatter->asTime($value, 'h:i:s')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asTime(null)); + } + + public function testAsDatetime() + { + $value = time(); + $this->assertSame(date('Y/m/d h:i:s A', $value), $this->formatter->asDatetime($value)); + $this->assertSame(date('Y-m-d h:i:s', $value), $this->formatter->asDatetime($value, 'Y-m-d h:i:s')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); + } + + public function testAsInteger() + { + $value = 123; + $this->assertSame("$value", $this->formatter->asInteger($value)); + $value = 123.23; + $this->assertSame("123", $this->formatter->asInteger($value)); + $value = 'a'; + $this->assertSame("0", $this->formatter->asInteger($value)); + $value = -123.23; + $this->assertSame("-123", $this->formatter->asInteger($value)); + $value = "-123abc"; + $this->assertSame("-123", $this->formatter->asInteger($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null)); + } + + public function testAsDouble() + { + $value = 123.12; + $this->assertSame("123.12", $this->formatter->asDouble($value)); + $this->assertSame("123.1", $this->formatter->asDouble($value, 1)); + $this->assertSame("123", $this->formatter->asDouble($value, 0)); + $value = 123; + $this->assertSame("123.00", $this->formatter->asDouble($value)); + $this->formatter->decimalSeparator = ','; + $value = 123.12; + $this->assertSame("123,12", $this->formatter->asDouble($value)); + $this->assertSame("123,1", $this->formatter->asDouble($value, 1)); + $this->assertSame("123", $this->formatter->asDouble($value, 0)); + $value = 123123.123; + $this->assertSame("123123,12", $this->formatter->asDouble($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDouble(null)); + } + + public function testAsNumber() + { + $value = 123123.123; + $this->assertSame("123,123", $this->formatter->asNumber($value)); + $this->assertSame("123,123.12", $this->formatter->asNumber($value, 2)); + $this->formatter->decimalSeparator = ','; + $this->formatter->thousandSeparator = ' '; + $this->assertSame("123 123", $this->formatter->asNumber($value)); + $this->assertSame("123 123,12", $this->formatter->asNumber($value, 2)); + $this->formatter->thousandSeparator = ''; + $this->assertSame("123123", $this->formatter->asNumber($value)); + $this->assertSame("123123,12", $this->formatter->asNumber($value, 2)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNumber(null)); + } + + public function testFormat() + { + $value = time(); + $this->assertSame(date('Y/m/d', $value), $this->formatter->format($value, 'date')); + $this->assertSame(date('Y/m/d', $value), $this->formatter->format($value, 'DATE')); + $this->assertSame(date('Y-m-d', $value), $this->formatter->format($value, ['date', 'Y-m-d'])); + $this->setExpectedException('\yii\base\InvalidParamException'); + $this->assertSame(date('Y-m-d', $value), $this->formatter->format($value, 'data')); + } } diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index 88926a93d51..f64afd7ed3b 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -13,262 +13,262 @@ */ class ModelTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - public function testGetAttributeLabel() - { - $speaker = new Speaker(); - $this->assertEquals('First Name', $speaker->getAttributeLabel('firstName')); - $this->assertEquals('This is the custom label', $speaker->getAttributeLabel('customLabel')); - $this->assertEquals('Underscore Style', $speaker->getAttributeLabel('underscore_style')); - } - - public function testGetAttributes() - { - $speaker = new Speaker(); - $speaker->firstName = 'Qiang'; - $speaker->lastName = 'Xue'; - - $this->assertEquals([ - 'firstName' => 'Qiang', - 'lastName' => 'Xue', - 'customLabel' => null, - 'underscore_style' => null, - ], $speaker->getAttributes()); - - $this->assertEquals([ - 'firstName' => 'Qiang', - 'lastName' => 'Xue', - ], $speaker->getAttributes(['firstName', 'lastName'])); - - $this->assertEquals([ - 'firstName' => 'Qiang', - 'lastName' => 'Xue', - ], $speaker->getAttributes(null, ['customLabel', 'underscore_style'])); - - $this->assertEquals([ - 'firstName' => 'Qiang', - ], $speaker->getAttributes(['firstName', 'lastName'], ['lastName', 'customLabel', 'underscore_style'])); - } - - public function testSetAttributes() - { - // by default mass assignment doesn't work at all - $speaker = new Speaker(); - $speaker->setAttributes(['firstName' => 'Qiang', 'underscore_style' => 'test']); - $this->assertNull($speaker->firstName); - $this->assertNull($speaker->underscore_style); - - // in the test scenario - $speaker = new Speaker(); - $speaker->setScenario('test'); - $speaker->setAttributes(['firstName' => 'Qiang', 'underscore_style' => 'test']); - $this->assertNull($speaker->underscore_style); - $this->assertEquals('Qiang', $speaker->firstName); - - $speaker->setAttributes(['firstName' => 'Qiang', 'underscore_style' => 'test'], false); - $this->assertEquals('test', $speaker->underscore_style); - $this->assertEquals('Qiang', $speaker->firstName); - } - - public function testLoad() - { - $singer = new Singer(); - $this->assertEquals('Singer', $singer->formName()); - - $post = ['firstName' => 'Qiang']; - - Speaker::$formName = ''; - $model = new Speaker(); - $model->setScenario('test'); - $this->assertTrue($model->load($post)); - $this->assertEquals('Qiang', $model->firstName); - - Speaker::$formName = 'Speaker'; - $model = new Speaker(); - $model->setScenario('test'); - $this->assertTrue($model->load(['Speaker' => $post])); - $this->assertEquals('Qiang', $model->firstName); - - Speaker::$formName = 'Speaker'; - $model = new Speaker(); - $model->setScenario('test'); - $this->assertFalse($model->load(['Example' => []])); - $this->assertEquals('', $model->firstName); - } - - public function testActiveAttributes() - { - // by default mass assignment doesn't work at all - $speaker = new Speaker(); - $this->assertEmpty($speaker->activeAttributes()); - - $speaker = new Speaker(); - $speaker->setScenario('test'); - $this->assertEquals(['firstName', 'lastName', 'underscore_style'], $speaker->activeAttributes()); - } - - public function testIsAttributeSafe() - { - // by default mass assignment doesn't work at all - $speaker = new Speaker(); - $this->assertFalse($speaker->isAttributeSafe('firstName')); - - $speaker = new Speaker(); - $speaker->setScenario('test'); - $this->assertTrue($speaker->isAttributeSafe('firstName')); - - } - - public function testErrors() - { - $speaker = new Speaker(); - - $this->assertEmpty($speaker->getErrors()); - $this->assertEmpty($speaker->getErrors('firstName')); - $this->assertEmpty($speaker->getFirstErrors()); - - $this->assertFalse($speaker->hasErrors()); - $this->assertFalse($speaker->hasErrors('firstName')); - - $speaker->addError('firstName', 'Something is wrong!'); - $this->assertEquals(['firstName' => ['Something is wrong!']], $speaker->getErrors()); - $this->assertEquals(['Something is wrong!'], $speaker->getErrors('firstName')); - - $speaker->addError('firstName', 'Totally wrong!'); - $this->assertEquals(['firstName' => ['Something is wrong!', 'Totally wrong!']], $speaker->getErrors()); - $this->assertEquals(['Something is wrong!', 'Totally wrong!'], $speaker->getErrors('firstName')); - - $this->assertTrue($speaker->hasErrors()); - $this->assertTrue($speaker->hasErrors('firstName')); - $this->assertFalse($speaker->hasErrors('lastName')); - - $this->assertEquals(['firstName' => 'Something is wrong!'], $speaker->getFirstErrors()); - $this->assertEquals('Something is wrong!', $speaker->getFirstError('firstName')); - $this->assertNull($speaker->getFirstError('lastName')); - - $speaker->addError('lastName', 'Another one!'); - $this->assertEquals([ - 'firstName' => [ - 'Something is wrong!', - 'Totally wrong!', - ], - 'lastName' => ['Another one!'], - ], $speaker->getErrors()); - - $speaker->clearErrors('firstName'); - $this->assertEquals([ - 'lastName' => ['Another one!'], - ], $speaker->getErrors()); - - $speaker->clearErrors(); - $this->assertEmpty($speaker->getErrors()); - $this->assertFalse($speaker->hasErrors()); - } - - public function testArraySyntax() - { - $speaker = new Speaker(); - - // get - $this->assertNull($speaker['firstName']); - - // isset - $this->assertFalse(isset($speaker['firstName'])); - - // set - $speaker['firstName'] = 'Qiang'; - - $this->assertEquals('Qiang', $speaker['firstName']); - $this->assertTrue(isset($speaker['firstName'])); - - // iteration - $attributes = []; - foreach ($speaker as $key => $attribute) { - $attributes[$key] = $attribute; - } - $this->assertEquals([ - 'firstName' => 'Qiang', - 'lastName' => null, - 'customLabel' => null, - 'underscore_style' => null, - ], $attributes); - - // unset - unset($speaker['firstName']); - - // exception isn't expected here - $this->assertNull($speaker['firstName']); - $this->assertFalse(isset($speaker['firstName'])); - } - - public function testDefaults() - { - $singer = new Model(); - $this->assertEquals([], $singer->rules()); - $this->assertEquals([], $singer->attributeLabels()); - } - - public function testDefaultScenarios() - { - $singer = new Singer(); - $this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios()); - - $scenarios = [ - 'default' => ['id', 'name', 'description'], - 'administration' => ['name', 'description', 'is_disabled'], - ]; - $model = new ComplexModel1(); - $this->assertEquals($scenarios, $model->scenarios()); - $scenarios = [ - 'default' => ['id', 'name', 'description'], - 'suddenlyUnexpectedScenario' => ['name', 'description'], - 'administration' => ['id', 'name', 'description', 'is_disabled'], - ]; - $model = new ComplexModel2(); - $this->assertEquals($scenarios, $model->scenarios()); - } - - public function testIsAttributeRequired() - { - $singer = new Singer(); - $this->assertFalse($singer->isAttributeRequired('firstName')); - $this->assertTrue($singer->isAttributeRequired('lastName')); - } - - public function testCreateValidators() - { - $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); - - $invalid = new InvalidRulesModel(); - $invalid->createValidators(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testGetAttributeLabel() + { + $speaker = new Speaker(); + $this->assertEquals('First Name', $speaker->getAttributeLabel('firstName')); + $this->assertEquals('This is the custom label', $speaker->getAttributeLabel('customLabel')); + $this->assertEquals('Underscore Style', $speaker->getAttributeLabel('underscore_style')); + } + + public function testGetAttributes() + { + $speaker = new Speaker(); + $speaker->firstName = 'Qiang'; + $speaker->lastName = 'Xue'; + + $this->assertEquals([ + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + 'customLabel' => null, + 'underscore_style' => null, + ], $speaker->getAttributes()); + + $this->assertEquals([ + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + ], $speaker->getAttributes(['firstName', 'lastName'])); + + $this->assertEquals([ + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + ], $speaker->getAttributes(null, ['customLabel', 'underscore_style'])); + + $this->assertEquals([ + 'firstName' => 'Qiang', + ], $speaker->getAttributes(['firstName', 'lastName'], ['lastName', 'customLabel', 'underscore_style'])); + } + + public function testSetAttributes() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $speaker->setAttributes(['firstName' => 'Qiang', 'underscore_style' => 'test']); + $this->assertNull($speaker->firstName); + $this->assertNull($speaker->underscore_style); + + // in the test scenario + $speaker = new Speaker(); + $speaker->setScenario('test'); + $speaker->setAttributes(['firstName' => 'Qiang', 'underscore_style' => 'test']); + $this->assertNull($speaker->underscore_style); + $this->assertEquals('Qiang', $speaker->firstName); + + $speaker->setAttributes(['firstName' => 'Qiang', 'underscore_style' => 'test'], false); + $this->assertEquals('test', $speaker->underscore_style); + $this->assertEquals('Qiang', $speaker->firstName); + } + + public function testLoad() + { + $singer = new Singer(); + $this->assertEquals('Singer', $singer->formName()); + + $post = ['firstName' => 'Qiang']; + + Speaker::$formName = ''; + $model = new Speaker(); + $model->setScenario('test'); + $this->assertTrue($model->load($post)); + $this->assertEquals('Qiang', $model->firstName); + + Speaker::$formName = 'Speaker'; + $model = new Speaker(); + $model->setScenario('test'); + $this->assertTrue($model->load(['Speaker' => $post])); + $this->assertEquals('Qiang', $model->firstName); + + Speaker::$formName = 'Speaker'; + $model = new Speaker(); + $model->setScenario('test'); + $this->assertFalse($model->load(['Example' => []])); + $this->assertEquals('', $model->firstName); + } + + public function testActiveAttributes() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $this->assertEmpty($speaker->activeAttributes()); + + $speaker = new Speaker(); + $speaker->setScenario('test'); + $this->assertEquals(['firstName', 'lastName', 'underscore_style'], $speaker->activeAttributes()); + } + + public function testIsAttributeSafe() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $this->assertFalse($speaker->isAttributeSafe('firstName')); + + $speaker = new Speaker(); + $speaker->setScenario('test'); + $this->assertTrue($speaker->isAttributeSafe('firstName')); + + } + + public function testErrors() + { + $speaker = new Speaker(); + + $this->assertEmpty($speaker->getErrors()); + $this->assertEmpty($speaker->getErrors('firstName')); + $this->assertEmpty($speaker->getFirstErrors()); + + $this->assertFalse($speaker->hasErrors()); + $this->assertFalse($speaker->hasErrors('firstName')); + + $speaker->addError('firstName', 'Something is wrong!'); + $this->assertEquals(['firstName' => ['Something is wrong!']], $speaker->getErrors()); + $this->assertEquals(['Something is wrong!'], $speaker->getErrors('firstName')); + + $speaker->addError('firstName', 'Totally wrong!'); + $this->assertEquals(['firstName' => ['Something is wrong!', 'Totally wrong!']], $speaker->getErrors()); + $this->assertEquals(['Something is wrong!', 'Totally wrong!'], $speaker->getErrors('firstName')); + + $this->assertTrue($speaker->hasErrors()); + $this->assertTrue($speaker->hasErrors('firstName')); + $this->assertFalse($speaker->hasErrors('lastName')); + + $this->assertEquals(['firstName' => 'Something is wrong!'], $speaker->getFirstErrors()); + $this->assertEquals('Something is wrong!', $speaker->getFirstError('firstName')); + $this->assertNull($speaker->getFirstError('lastName')); + + $speaker->addError('lastName', 'Another one!'); + $this->assertEquals([ + 'firstName' => [ + 'Something is wrong!', + 'Totally wrong!', + ], + 'lastName' => ['Another one!'], + ], $speaker->getErrors()); + + $speaker->clearErrors('firstName'); + $this->assertEquals([ + 'lastName' => ['Another one!'], + ], $speaker->getErrors()); + + $speaker->clearErrors(); + $this->assertEmpty($speaker->getErrors()); + $this->assertFalse($speaker->hasErrors()); + } + + public function testArraySyntax() + { + $speaker = new Speaker(); + + // get + $this->assertNull($speaker['firstName']); + + // isset + $this->assertFalse(isset($speaker['firstName'])); + + // set + $speaker['firstName'] = 'Qiang'; + + $this->assertEquals('Qiang', $speaker['firstName']); + $this->assertTrue(isset($speaker['firstName'])); + + // iteration + $attributes = []; + foreach ($speaker as $key => $attribute) { + $attributes[$key] = $attribute; + } + $this->assertEquals([ + 'firstName' => 'Qiang', + 'lastName' => null, + 'customLabel' => null, + 'underscore_style' => null, + ], $attributes); + + // unset + unset($speaker['firstName']); + + // exception isn't expected here + $this->assertNull($speaker['firstName']); + $this->assertFalse(isset($speaker['firstName'])); + } + + public function testDefaults() + { + $singer = new Model(); + $this->assertEquals([], $singer->rules()); + $this->assertEquals([], $singer->attributeLabels()); + } + + public function testDefaultScenarios() + { + $singer = new Singer(); + $this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios()); + + $scenarios = [ + 'default' => ['id', 'name', 'description'], + 'administration' => ['name', 'description', 'is_disabled'], + ]; + $model = new ComplexModel1(); + $this->assertEquals($scenarios, $model->scenarios()); + $scenarios = [ + 'default' => ['id', 'name', 'description'], + 'suddenlyUnexpectedScenario' => ['name', 'description'], + 'administration' => ['id', 'name', 'description', 'is_disabled'], + ]; + $model = new ComplexModel2(); + $this->assertEquals($scenarios, $model->scenarios()); + } + + public function testIsAttributeRequired() + { + $singer = new Singer(); + $this->assertFalse($singer->isAttributeRequired('firstName')); + $this->assertTrue($singer->isAttributeRequired('lastName')); + } + + public function testCreateValidators() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); + + $invalid = new InvalidRulesModel(); + $invalid->createValidators(); + } } class ComplexModel1 extends Model { - public function rules() - { - return [ - [['id'], 'required', 'except' => 'administration'], - [['name', 'description'], 'filter', 'filter' => 'trim'], - [['is_disabled'], 'boolean', 'on' => 'administration'], - ]; - } + public function rules() + { + return [ + [['id'], 'required', 'except' => 'administration'], + [['name', 'description'], 'filter', 'filter' => 'trim'], + [['is_disabled'], 'boolean', 'on' => 'administration'], + ]; + } } class ComplexModel2 extends Model { - public function rules() - { - return [ - [['id'], 'required', 'except' => 'suddenlyUnexpectedScenario'], - [['name', 'description'], 'filter', 'filter' => 'trim'], - [['is_disabled'], 'boolean', 'on' => 'administration'], - ]; - } + public function rules() + { + return [ + [['id'], 'required', 'except' => 'suddenlyUnexpectedScenario'], + [['name', 'description'], 'filter', 'filter' => 'trim'], + [['is_disabled'], 'boolean', 'on' => 'administration'], + ]; + } } diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index 1a379c9145c..06968f09523 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -9,191 +9,192 @@ */ class ObjectTest extends TestCase { - /** - * @var NewObject - */ - protected $object; - - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $this->object = new NewObject; - } - - protected function tearDown() - { - parent::tearDown(); - $this->object = null; - } - - public function testHasProperty() - { - $this->assertTrue($this->object->hasProperty('Text')); - $this->assertTrue($this->object->hasProperty('text')); - $this->assertFalse($this->object->hasProperty('Caption')); - $this->assertTrue($this->object->hasProperty('content')); - $this->assertFalse($this->object->hasProperty('content', false)); - $this->assertFalse($this->object->hasProperty('Content')); - } - - public function testCanGetProperty() - { - $this->assertTrue($this->object->canGetProperty('Text')); - $this->assertTrue($this->object->canGetProperty('text')); - $this->assertFalse($this->object->canGetProperty('Caption')); - $this->assertTrue($this->object->canGetProperty('content')); - $this->assertFalse($this->object->canGetProperty('content', false)); - $this->assertFalse($this->object->canGetProperty('Content')); - } - - public function testCanSetProperty() - { - $this->assertTrue($this->object->canSetProperty('Text')); - $this->assertTrue($this->object->canSetProperty('text')); - $this->assertFalse($this->object->canSetProperty('Object')); - $this->assertFalse($this->object->canSetProperty('Caption')); - $this->assertTrue($this->object->canSetProperty('content')); - $this->assertFalse($this->object->canSetProperty('content', false)); - $this->assertFalse($this->object->canSetProperty('Content')); - } - - public function testGetProperty() - { - $this->assertTrue('default' === $this->object->Text); - $this->setExpectedException('yii\base\UnknownPropertyException'); - $value2 = $this->object->Caption; - } - - public function testSetProperty() - { - $value = 'new value'; - $this->object->Text = $value; - $this->assertEquals($value, $this->object->Text); - $this->setExpectedException('yii\base\UnknownPropertyException'); - $this->object->NewMember = $value; - } - - public function testSetReadOnlyProperty() - { - $this->setExpectedException('yii\base\InvalidCallException'); - $this->object->object = 'test'; - } - - public function testIsset() - { - $this->assertTrue(isset($this->object->Text)); - $this->assertFalse(empty($this->object->Text)); - - $this->object->Text = ''; - $this->assertTrue(isset($this->object->Text)); - $this->assertTrue(empty($this->object->Text)); - - $this->object->Text = null; - $this->assertFalse(isset($this->object->Text)); - $this->assertTrue(empty($this->object->Text)); - - $this->assertFalse(isset($this->object->unknownProperty)); - $this->assertTrue(empty($this->object->unknownProperty)); - } - - public function testUnset() - { - unset($this->object->Text); - $this->assertFalse(isset($this->object->Text)); - $this->assertTrue(empty($this->object->Text)); - } - - public function testUnsetReadOnlyProperty() - { - $this->setExpectedException('yii\base\InvalidCallException'); - unset($this->object->object); - } - - public function testCallUnknownMethod() - { - $this->setExpectedException('yii\base\UnknownMethodException'); - $this->object->unknownMethod(); - } - - public function testArrayProperty() - { - $this->assertEquals([], $this->object->items); - // the following won't work - /* - $this->object->items[] = 1; - $this->assertEquals([1], $this->object->items); - */ - } - - public function testObjectProperty() - { - $this->assertTrue($this->object->object instanceof NewObject); - $this->assertEquals('object text', $this->object->object->text); - $this->object->object->text = 'new text'; - $this->assertEquals('new text', $this->object->object->text); - } - - public function testConstruct() - { - $object = new NewObject(['text' => 'test text']); - $this->assertEquals('test text', $object->getText()); - } - - public function testGetClassName() - { - $object = $this->object; - $this->assertSame(get_class($object), $object::className()); - } - - public function testReadingWriteOnlyProperty() - { - $this->setExpectedException( - 'yii\base\InvalidCallException', - 'Getting write-only property: yiiunit\framework\base\NewObject::writeOnly' - ); - $this->object->writeOnly; - } + /** + * @var NewObject + */ + protected $object; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->object = new NewObject; + } + + protected function tearDown() + { + parent::tearDown(); + $this->object = null; + } + + public function testHasProperty() + { + $this->assertTrue($this->object->hasProperty('Text')); + $this->assertTrue($this->object->hasProperty('text')); + $this->assertFalse($this->object->hasProperty('Caption')); + $this->assertTrue($this->object->hasProperty('content')); + $this->assertFalse($this->object->hasProperty('content', false)); + $this->assertFalse($this->object->hasProperty('Content')); + } + + public function testCanGetProperty() + { + $this->assertTrue($this->object->canGetProperty('Text')); + $this->assertTrue($this->object->canGetProperty('text')); + $this->assertFalse($this->object->canGetProperty('Caption')); + $this->assertTrue($this->object->canGetProperty('content')); + $this->assertFalse($this->object->canGetProperty('content', false)); + $this->assertFalse($this->object->canGetProperty('Content')); + } + + public function testCanSetProperty() + { + $this->assertTrue($this->object->canSetProperty('Text')); + $this->assertTrue($this->object->canSetProperty('text')); + $this->assertFalse($this->object->canSetProperty('Object')); + $this->assertFalse($this->object->canSetProperty('Caption')); + $this->assertTrue($this->object->canSetProperty('content')); + $this->assertFalse($this->object->canSetProperty('content', false)); + $this->assertFalse($this->object->canSetProperty('Content')); + } + + public function testGetProperty() + { + $this->assertTrue('default' === $this->object->Text); + $this->setExpectedException('yii\base\UnknownPropertyException'); + $value2 = $this->object->Caption; + } + + public function testSetProperty() + { + $value = 'new value'; + $this->object->Text = $value; + $this->assertEquals($value, $this->object->Text); + $this->setExpectedException('yii\base\UnknownPropertyException'); + $this->object->NewMember = $value; + } + + public function testSetReadOnlyProperty() + { + $this->setExpectedException('yii\base\InvalidCallException'); + $this->object->object = 'test'; + } + + public function testIsset() + { + $this->assertTrue(isset($this->object->Text)); + $this->assertFalse(empty($this->object->Text)); + + $this->object->Text = ''; + $this->assertTrue(isset($this->object->Text)); + $this->assertTrue(empty($this->object->Text)); + + $this->object->Text = null; + $this->assertFalse(isset($this->object->Text)); + $this->assertTrue(empty($this->object->Text)); + + $this->assertFalse(isset($this->object->unknownProperty)); + $this->assertTrue(empty($this->object->unknownProperty)); + } + + public function testUnset() + { + unset($this->object->Text); + $this->assertFalse(isset($this->object->Text)); + $this->assertTrue(empty($this->object->Text)); + } + + public function testUnsetReadOnlyProperty() + { + $this->setExpectedException('yii\base\InvalidCallException'); + unset($this->object->object); + } + + public function testCallUnknownMethod() + { + $this->setExpectedException('yii\base\UnknownMethodException'); + $this->object->unknownMethod(); + } + + public function testArrayProperty() + { + $this->assertEquals([], $this->object->items); + // the following won't work + /* + $this->object->items[] = 1; + $this->assertEquals([1], $this->object->items); + */ + } + + public function testObjectProperty() + { + $this->assertTrue($this->object->object instanceof NewObject); + $this->assertEquals('object text', $this->object->object->text); + $this->object->object->text = 'new text'; + $this->assertEquals('new text', $this->object->object->text); + } + + public function testConstruct() + { + $object = new NewObject(['text' => 'test text']); + $this->assertEquals('test text', $object->getText()); + } + + public function testGetClassName() + { + $object = $this->object; + $this->assertSame(get_class($object), $object::className()); + } + + public function testReadingWriteOnlyProperty() + { + $this->setExpectedException( + 'yii\base\InvalidCallException', + 'Getting write-only property: yiiunit\framework\base\NewObject::writeOnly' + ); + $this->object->writeOnly; + } } class NewObject extends Object { - private $_object = null; - private $_text = 'default'; - private $_items = []; - public $content; - - public function getText() - { - return $this->_text; - } - - public function setText($value) - { - $this->_text = $value; - } - - public function getObject() - { - if (!$this->_object) { - $this->_object = new self; - $this->_object->_text = 'object text'; - } - return $this->_object; - } - - public function getExecute() - { - return function ($param) { - return $param * 2; - }; - } - - public function getItems() - { - return $this->_items; - } - - public function setWriteOnly(){} + private $_object = null; + private $_text = 'default'; + private $_items = []; + public $content; + + public function getText() + { + return $this->_text; + } + + public function setText($value) + { + $this->_text = $value; + } + + public function getObject() + { + if (!$this->_object) { + $this->_object = new self; + $this->_object->_text = 'object text'; + } + + return $this->_object; + } + + public function getExecute() + { + return function ($param) { + return $param * 2; + }; + } + + public function getItems() + { + return $this->_items; + } + + public function setWriteOnly() {} } diff --git a/tests/unit/framework/behaviors/TimestampBehaviorTest.php b/tests/unit/framework/behaviors/TimestampBehaviorTest.php index 8a46215704a..40d68864064 100644 --- a/tests/unit/framework/behaviors/TimestampBehaviorTest.php +++ b/tests/unit/framework/behaviors/TimestampBehaviorTest.php @@ -16,75 +16,75 @@ */ class TimestampBehaviorTest extends TestCase { - /** - * @var Connection test db connection - */ - protected $dbConnection; - - public static function setUpBeforeClass() - { - if (!extension_loaded('pdo') || !extension_loaded('pdo_sqlite')) { - static::markTestSkipped('PDO and SQLite extensions are required.'); - } - } - - public function setUp() - { - $this->mockApplication([ - 'components' => [ - 'db' => [ - 'class' => '\yii\db\Connection', - 'dsn' => 'sqlite::memory:', - ] - ] - ]); - - $columns = [ - 'id' => 'pk', - 'created_at' => 'integer', - 'updated_at' => 'integer', - ]; - Yii::$app->getDb()->createCommand()->createTable('test_auto_timestamp', $columns)->execute(); - } - - public function tearDown() - { - Yii::$app->getDb()->close(); - parent::tearDown(); - } - - // Tests : - - public function testNewRecord() - { - $currentTime = time(); - - $model = new ActiveRecordTimestamp(); - $model->save(false); - - $this->assertTrue($model->created_at >= $currentTime); - $this->assertTrue($model->updated_at >= $currentTime); - } - - /** - * @depends testNewRecord - */ - public function testUpdateRecord() - { - $currentTime = time(); - - $model = new ActiveRecordTimestamp(); - $model->save(false); - - $enforcedTime = $currentTime - 100; - - $model->created_at = $enforcedTime; - $model->updated_at = $enforcedTime; - $model->save(false); - - $this->assertEquals($enforcedTime, $model->created_at, 'Create time has been set on update!'); - $this->assertTrue($model->updated_at >= $currentTime, 'Update time has NOT been set on update!'); - } + /** + * @var Connection test db connection + */ + protected $dbConnection; + + public static function setUpBeforeClass() + { + if (!extension_loaded('pdo') || !extension_loaded('pdo_sqlite')) { + static::markTestSkipped('PDO and SQLite extensions are required.'); + } + } + + public function setUp() + { + $this->mockApplication([ + 'components' => [ + 'db' => [ + 'class' => '\yii\db\Connection', + 'dsn' => 'sqlite::memory:', + ] + ] + ]); + + $columns = [ + 'id' => 'pk', + 'created_at' => 'integer', + 'updated_at' => 'integer', + ]; + Yii::$app->getDb()->createCommand()->createTable('test_auto_timestamp', $columns)->execute(); + } + + public function tearDown() + { + Yii::$app->getDb()->close(); + parent::tearDown(); + } + + // Tests : + + public function testNewRecord() + { + $currentTime = time(); + + $model = new ActiveRecordTimestamp(); + $model->save(false); + + $this->assertTrue($model->created_at >= $currentTime); + $this->assertTrue($model->updated_at >= $currentTime); + } + + /** + * @depends testNewRecord + */ + public function testUpdateRecord() + { + $currentTime = time(); + + $model = new ActiveRecordTimestamp(); + $model->save(false); + + $enforcedTime = $currentTime - 100; + + $model->created_at = $enforcedTime; + $model->updated_at = $enforcedTime; + $model->save(false); + + $this->assertEquals($enforcedTime, $model->created_at, 'Create time has been set on update!'); + $this->assertTrue($model->updated_at >= $currentTime, 'Update time has NOT been set on update!'); + } } /** @@ -96,15 +96,15 @@ public function testUpdateRecord() */ class ActiveRecordTimestamp extends ActiveRecord { - public function behaviors() - { - return [ - TimestampBehavior::className(), - ]; - } - - public static function tableName() - { - return 'test_auto_timestamp'; - } + public function behaviors() + { + return [ + TimestampBehavior::className(), + ]; + } + + public static function tableName() + { + return 'test_auto_timestamp'; + } } diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php index b65ec412c3f..126471c5e6f 100644 --- a/tests/unit/framework/caching/ApcCacheTest.php +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -10,36 +10,37 @@ */ class ApcCacheTest extends CacheTestCase { - private $_cacheInstance = null; - - /** - * @return ApcCache - */ - protected function getCacheInstance() - { - if (!extension_loaded("apc")) { - $this->markTestSkipped("APC not installed. Skipping."); - } elseif ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { - $this->markTestSkipped("APC cli is not enabled. Skipping."); - } - - if (!ini_get("apc.enabled") || !ini_get("apc.enable_cli")) { - $this->markTestSkipped("APC is installed but not enabled. Skipping."); - } - - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new ApcCache(); - } - return $this->_cacheInstance; - } - - public function testExpire() - { - $this->markTestSkipped("APC keys are expiring only on the next request."); - } - - public function testExpireAdd() - { - $this->markTestSkipped("APC keys are expiring only on the next request."); - } + private $_cacheInstance = null; + + /** + * @return ApcCache + */ + protected function getCacheInstance() + { + if (!extension_loaded("apc")) { + $this->markTestSkipped("APC not installed. Skipping."); + } elseif ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { + $this->markTestSkipped("APC cli is not enabled. Skipping."); + } + + if (!ini_get("apc.enabled") || !ini_get("apc.enable_cli")) { + $this->markTestSkipped("APC is installed but not enabled. Skipping."); + } + + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new ApcCache(); + } + + return $this->_cacheInstance; + } + + public function testExpire() + { + $this->markTestSkipped("APC keys are expiring only on the next request."); + } + + public function testExpireAdd() + { + $this->markTestSkipped("APC keys are expiring only on the next request."); + } } diff --git a/tests/unit/framework/caching/CacheTestCase.php b/tests/unit/framework/caching/CacheTestCase.php index 0a8737df924..0e595b9e805 100644 --- a/tests/unit/framework/caching/CacheTestCase.php +++ b/tests/unit/framework/caching/CacheTestCase.php @@ -8,232 +8,231 @@ */ function time() { - return \yiiunit\framework\caching\CacheTestCase::$time ?: \time(); + return \yiiunit\framework\caching\CacheTestCase::$time ?: \time(); } namespace yiiunit\framework\caching; use yiiunit\TestCase; -use yii\caching\Cache; /** * Base class for testing cache backends */ abstract class CacheTestCase extends TestCase { - /** - * @var integer virtual time to be returned by mocked time() function. - * Null means normal time() behavior. - */ - public static $time; - - /** - * @return Cache - */ - abstract protected function getCacheInstance(); - - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - protected function tearDown() - { - static::$time = null; - } - - /** - * @return Cache - */ - public function prepare() - { - $cache = $this->getCacheInstance(); - - $cache->flush(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 42); - $cache->set('array_test', ['array_test' => 'array_test']); - $cache['arrayaccess_test'] = new \stdClass(); - - return $cache; - } - - /** - * default value of cache prefix is application id - */ - public function testKeyPrefix() - { - $cache = $this->getCacheInstance(); - $this->assertNotNull(\Yii::$app->id); - $this->assertNotNull($cache->keyPrefix); - } - - public function testSet() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->set('string_test', 'string_test')); - $this->assertTrue($cache->set('number_test', 42)); - $this->assertTrue($cache->set('array_test', ['array_test' => 'array_test'])); - } - - public function testGet() - { - $cache = $this->prepare(); - - $this->assertEquals('string_test', $cache->get('string_test')); - - $this->assertEquals(42, $cache->get('number_test')); - - $array = $cache->get('array_test'); - $this->assertArrayHasKey('array_test', $array); - $this->assertEquals('array_test', $array['array_test']); - } - - /** - * @return array testing mset with and without expiry - */ - public function msetExpiry() - { - return [[0], [2]]; - } - - /** - * @dataProvider msetExpiry - */ - public function testMset($expiry) - { - $cache = $this->getCacheInstance(); - $cache->flush(); - - $cache->mset([ - 'string_test' => 'string_test', - 'number_test' => 42, - 'array_test' => ['array_test' => 'array_test'], - ], $expiry); - - $this->assertEquals('string_test', $cache->get('string_test')); - - $this->assertEquals(42, $cache->get('number_test')); - - $array = $cache->get('array_test'); - $this->assertArrayHasKey('array_test', $array); - $this->assertEquals('array_test', $array['array_test']); - } - - public function testExists() - { - $cache = $this->prepare(); - - $this->assertTrue($cache->exists('string_test')); - // check whether exists affects the value - $this->assertEquals('string_test', $cache->get('string_test')); - - $this->assertTrue($cache->exists('number_test')); - $this->assertFalse($cache->exists('not_exists')); - } - - public function testArrayAccess() - { - $cache = $this->getCacheInstance(); - - $cache['arrayaccess_test'] = new \stdClass(); - $this->assertInstanceOf('stdClass', $cache['arrayaccess_test']); - } - - public function testGetNonExistent() - { - $cache = $this->getCacheInstance(); - - $this->assertFalse($cache->get('non_existent_key')); - } - - public function testStoreSpecialValues() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->set('null_value', null)); - $this->assertNull($cache->get('null_value')); - - $this->assertTrue($cache->set('bool_value', true)); - $this->assertTrue($cache->get('bool_value')); - } - - public function testMget() - { - $cache = $this->prepare(); - - $this->assertEquals(['string_test' => 'string_test', 'number_test' => 42], $cache->mget(['string_test', 'number_test'])); - // ensure that order does not matter - $this->assertEquals(['number_test' => 42, 'string_test' => 'string_test'], $cache->mget(['number_test', 'string_test'])); - $this->assertEquals(['number_test' => 42, 'non_existent_key' => null], $cache->mget(['number_test', 'non_existent_key'])); - } - - public function testExpire() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); - usleep(500000); - $this->assertEquals('expire_test', $cache->get('expire_test')); - usleep(2500000); - $this->assertFalse($cache->get('expire_test')); - } - - public function testExpireAdd() - { - $cache = $this->getCacheInstance(); - - $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); - usleep(500000); - $this->assertEquals('expire_testa', $cache->get('expire_testa')); - usleep(2500000); - $this->assertFalse($cache->get('expire_testa')); - } - - public function testAdd() - { - $cache = $this->prepare(); - - // should not change existing keys - $this->assertFalse($cache->add('number_test', 13)); - $this->assertEquals(42, $cache->get('number_test')); - - // should store data if it's not there yet - $this->assertFalse($cache->get('add_test')); - $this->assertTrue($cache->add('add_test', 13)); - $this->assertEquals(13, $cache->get('add_test')); - } - - public function testMadd() - { - $cache = $this->prepare(); - - $this->assertFalse($cache->get('add_test')); - - $cache->madd([ - 'number_test' => 13, - 'add_test' => 13, - ]); - - $this->assertEquals(42, $cache->get('number_test')); - $this->assertEquals(13, $cache->get('add_test')); - } - - public function testDelete() - { - $cache = $this->prepare(); - - $this->assertNotNull($cache->get('number_test')); - $this->assertTrue($cache->delete('number_test')); - $this->assertFalse($cache->get('number_test')); - } - - public function testFlush() - { - $cache = $this->prepare(); - $this->assertTrue($cache->flush()); - $this->assertFalse($cache->get('number_test')); - } + /** + * @var integer virtual time to be returned by mocked time() function. + * Null means normal time() behavior. + */ + public static $time; + + /** + * @return Cache + */ + abstract protected function getCacheInstance(); + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + protected function tearDown() + { + static::$time = null; + } + + /** + * @return Cache + */ + public function prepare() + { + $cache = $this->getCacheInstance(); + + $cache->flush(); + $cache->set('string_test', 'string_test'); + $cache->set('number_test', 42); + $cache->set('array_test', ['array_test' => 'array_test']); + $cache['arrayaccess_test'] = new \stdClass(); + + return $cache; + } + + /** + * default value of cache prefix is application id + */ + public function testKeyPrefix() + { + $cache = $this->getCacheInstance(); + $this->assertNotNull(\Yii::$app->id); + $this->assertNotNull($cache->keyPrefix); + } + + public function testSet() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 42)); + $this->assertTrue($cache->set('array_test', ['array_test' => 'array_test'])); + } + + public function testGet() + { + $cache = $this->prepare(); + + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertEquals(42, $cache->get('number_test')); + + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + } + + /** + * @return array testing mset with and without expiry + */ + public function msetExpiry() + { + return [[0], [2]]; + } + + /** + * @dataProvider msetExpiry + */ + public function testMset($expiry) + { + $cache = $this->getCacheInstance(); + $cache->flush(); + + $cache->mset([ + 'string_test' => 'string_test', + 'number_test' => 42, + 'array_test' => ['array_test' => 'array_test'], + ], $expiry); + + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertEquals(42, $cache->get('number_test')); + + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + } + + public function testExists() + { + $cache = $this->prepare(); + + $this->assertTrue($cache->exists('string_test')); + // check whether exists affects the value + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertTrue($cache->exists('number_test')); + $this->assertFalse($cache->exists('not_exists')); + } + + public function testArrayAccess() + { + $cache = $this->getCacheInstance(); + + $cache['arrayaccess_test'] = new \stdClass(); + $this->assertInstanceOf('stdClass', $cache['arrayaccess_test']); + } + + public function testGetNonExistent() + { + $cache = $this->getCacheInstance(); + + $this->assertFalse($cache->get('non_existent_key')); + } + + public function testStoreSpecialValues() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('null_value', null)); + $this->assertNull($cache->get('null_value')); + + $this->assertTrue($cache->set('bool_value', true)); + $this->assertTrue($cache->get('bool_value')); + } + + public function testMget() + { + $cache = $this->prepare(); + + $this->assertEquals(['string_test' => 'string_test', 'number_test' => 42], $cache->mget(['string_test', 'number_test'])); + // ensure that order does not matter + $this->assertEquals(['number_test' => 42, 'string_test' => 'string_test'], $cache->mget(['number_test', 'string_test'])); + $this->assertEquals(['number_test' => 42, 'non_existent_key' => null], $cache->mget(['number_test', 'non_existent_key'])); + } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); + usleep(500000); + $this->assertEquals('expire_test', $cache->get('expire_test')); + usleep(2500000); + $this->assertFalse($cache->get('expire_test')); + } + + public function testExpireAdd() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); + usleep(500000); + $this->assertEquals('expire_testa', $cache->get('expire_testa')); + usleep(2500000); + $this->assertFalse($cache->get('expire_testa')); + } + + public function testAdd() + { + $cache = $this->prepare(); + + // should not change existing keys + $this->assertFalse($cache->add('number_test', 13)); + $this->assertEquals(42, $cache->get('number_test')); + + // should store data if it's not there yet + $this->assertFalse($cache->get('add_test')); + $this->assertTrue($cache->add('add_test', 13)); + $this->assertEquals(13, $cache->get('add_test')); + } + + public function testMadd() + { + $cache = $this->prepare(); + + $this->assertFalse($cache->get('add_test')); + + $cache->madd([ + 'number_test' => 13, + 'add_test' => 13, + ]); + + $this->assertEquals(42, $cache->get('number_test')); + $this->assertEquals(13, $cache->get('add_test')); + } + + public function testDelete() + { + $cache = $this->prepare(); + + $this->assertNotNull($cache->get('number_test')); + $this->assertTrue($cache->delete('number_test')); + $this->assertFalse($cache->get('number_test')); + } + + public function testFlush() + { + $cache = $this->prepare(); + $this->assertTrue($cache->flush()); + $this->assertFalse($cache->get('number_test')); + } } diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index bfb30faeaf6..d8a02d628a2 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -11,88 +11,89 @@ */ class DbCacheTest extends CacheTestCase { - private $_cacheInstance; - private $_connection; + private $_cacheInstance; + private $_connection; - protected function setUp() - { - if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { - $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); - } + protected function setUp() + { + if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { + $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); + } - parent::setUp(); - - $this->getConnection()->createCommand(" - CREATE TABLE IF NOT EXISTS tbl_cache ( - id char(128) NOT NULL, - expire int(11) DEFAULT NULL, - data LONGBLOB, - PRIMARY KEY (id), - KEY expire (expire) - ); - ")->execute(); - } + parent::setUp(); - /** - * @param boolean $reset whether to clean up the test database - * @return \yii\db\Connection - */ - public function getConnection($reset = true) - { - if ($this->_connection === null) { - $databases = $this->getParam('databases'); - $params = $databases['mysql']; - $db = new \yii\db\Connection; - $db->dsn = $params['dsn']; - $db->username = $params['username']; - $db->password = $params['password']; - if ($reset) { - $db->open(); - $lines = explode(';', file_get_contents($params['fixture'])); - foreach ($lines as $line) { - if (trim($line) !== '') { - $db->pdo->exec($line); - } - } - } - $this->_connection = $db; - } - return $this->_connection; - } + $this->getConnection()->createCommand(" + CREATE TABLE IF NOT EXISTS tbl_cache ( + id char(128) NOT NULL, + expire int(11) DEFAULT NULL, + data LONGBLOB, + PRIMARY KEY (id), + KEY expire (expire) + ); + ")->execute(); + } + /** + * @param boolean $reset whether to clean up the test database + * @return \yii\db\Connection + */ + public function getConnection($reset = true) + { + if ($this->_connection === null) { + $databases = $this->getParam('databases'); + $params = $databases['mysql']; + $db = new \yii\db\Connection; + $db->dsn = $params['dsn']; + $db->username = $params['username']; + $db->password = $params['password']; + if ($reset) { + $db->open(); + $lines = explode(';', file_get_contents($params['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + $this->_connection = $db; + } - /** - * @return DbCache - */ - protected function getCacheInstance() - { - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new DbCache(['db' => $this->getConnection()]); - } - return $this->_cacheInstance; - } + return $this->_connection; + } - public function testExpire() - { - $cache = $this->getCacheInstance(); + /** + * @return DbCache + */ + protected function getCacheInstance() + { + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new DbCache(['db' => $this->getConnection()]); + } - static::$time = \time(); - $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); - static::$time++; - $this->assertEquals('expire_test', $cache->get('expire_test')); - static::$time++; - $this->assertFalse($cache->get('expire_test')); - } + return $this->_cacheInstance; + } - public function testExpireAdd() - { - $cache = $this->getCacheInstance(); + public function testExpire() + { + $cache = $this->getCacheInstance(); - static::$time = \time(); - $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); - static::$time++; - $this->assertEquals('expire_testa', $cache->get('expire_testa')); - static::$time++; - $this->assertFalse($cache->get('expire_testa')); - } + static::$time = \time(); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); + static::$time++; + $this->assertEquals('expire_test', $cache->get('expire_test')); + static::$time++; + $this->assertFalse($cache->get('expire_test')); + } + + public function testExpireAdd() + { + $cache = $this->getCacheInstance(); + + static::$time = \time(); + $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); + static::$time++; + $this->assertEquals('expire_testa', $cache->get('expire_testa')); + static::$time++; + $this->assertFalse($cache->get('expire_testa')); + } } diff --git a/tests/unit/framework/caching/FileCacheTest.php b/tests/unit/framework/caching/FileCacheTest.php index f10261464e8..1c7e183f80f 100644 --- a/tests/unit/framework/caching/FileCacheTest.php +++ b/tests/unit/framework/caching/FileCacheTest.php @@ -9,40 +9,41 @@ */ class FileCacheTest extends CacheTestCase { - private $_cacheInstance = null; - - /** - * @return FileCache - */ - protected function getCacheInstance() - { - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new FileCache(['cachePath' => '@yiiunit/runtime/cache']); - } - return $this->_cacheInstance; - } - - public function testExpire() - { - $cache = $this->getCacheInstance(); - - static::$time = \time(); - $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); - static::$time++; - $this->assertEquals('expire_test', $cache->get('expire_test')); - static::$time++; - $this->assertFalse($cache->get('expire_test')); - } - - public function testExpireAdd() - { - $cache = $this->getCacheInstance(); - - static::$time = \time(); - $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); - static::$time++; - $this->assertEquals('expire_testa', $cache->get('expire_testa')); - static::$time++; - $this->assertFalse($cache->get('expire_testa')); - } + private $_cacheInstance = null; + + /** + * @return FileCache + */ + protected function getCacheInstance() + { + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new FileCache(['cachePath' => '@yiiunit/runtime/cache']); + } + + return $this->_cacheInstance; + } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + + static::$time = \time(); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); + static::$time++; + $this->assertEquals('expire_test', $cache->get('expire_test')); + static::$time++; + $this->assertFalse($cache->get('expire_test')); + } + + public function testExpireAdd() + { + $cache = $this->getCacheInstance(); + + static::$time = \time(); + $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); + static::$time++; + $this->assertEquals('expire_testa', $cache->get('expire_testa')); + static::$time++; + $this->assertFalse($cache->get('expire_testa')); + } } diff --git a/tests/unit/framework/caching/MemCacheTest.php b/tests/unit/framework/caching/MemCacheTest.php index 63f8be16402..36d79043791 100644 --- a/tests/unit/framework/caching/MemCacheTest.php +++ b/tests/unit/framework/caching/MemCacheTest.php @@ -10,36 +10,37 @@ */ class MemCacheTest extends CacheTestCase { - private $_cacheInstance = null; + private $_cacheInstance = null; - /** - * @return MemCache - */ - protected function getCacheInstance() - { - if (!extension_loaded("memcache")) { - $this->markTestSkipped("memcache not installed. Skipping."); - } + /** + * @return MemCache + */ + protected function getCacheInstance() + { + if (!extension_loaded("memcache")) { + $this->markTestSkipped("memcache not installed. Skipping."); + } - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new MemCache(); - } - return $this->_cacheInstance; - } + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new MemCache(); + } - public function testExpire() - { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcache expiry on travis-ci.'); - } - parent::testExpire(); - } + return $this->_cacheInstance; + } - public function testExpireAdd() - { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcache expiry on travis-ci.'); - } - parent::testExpireAdd(); - } + public function testExpire() + { + if (getenv('TRAVIS') == 'true') { + $this->markTestSkipped('Can not reliably test memcache expiry on travis-ci.'); + } + parent::testExpire(); + } + + public function testExpireAdd() + { + if (getenv('TRAVIS') == 'true') { + $this->markTestSkipped('Can not reliably test memcache expiry on travis-ci.'); + } + parent::testExpireAdd(); + } } diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php index 3a9d41530c9..35d9800f87d 100644 --- a/tests/unit/framework/caching/MemCachedTest.php +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -10,36 +10,37 @@ */ class MemCachedTest extends CacheTestCase { - private $_cacheInstance = null; + private $_cacheInstance = null; - /** - * @return MemCache - */ - protected function getCacheInstance() - { - if (!extension_loaded("memcached")) { - $this->markTestSkipped("memcached not installed. Skipping."); - } + /** + * @return MemCache + */ + protected function getCacheInstance() + { + if (!extension_loaded("memcached")) { + $this->markTestSkipped("memcached not installed. Skipping."); + } - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new MemCache(['useMemcached' => true]); - } - return $this->_cacheInstance; - } + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new MemCache(['useMemcached' => true]); + } - public function testExpire() - { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcached expiry on travis-ci.'); - } - parent::testExpire(); - } + return $this->_cacheInstance; + } - public function testExpireAdd() - { - if (getenv('TRAVIS') == 'true') { - $this->markTestSkipped('Can not reliably test memcached expiry on travis-ci.'); - } - parent::testExpireAdd(); - } + public function testExpire() + { + if (getenv('TRAVIS') == 'true') { + $this->markTestSkipped('Can not reliably test memcached expiry on travis-ci.'); + } + parent::testExpire(); + } + + public function testExpireAdd() + { + if (getenv('TRAVIS') == 'true') { + $this->markTestSkipped('Can not reliably test memcached expiry on travis-ci.'); + } + parent::testExpireAdd(); + } } diff --git a/tests/unit/framework/caching/WinCacheTest.php b/tests/unit/framework/caching/WinCacheTest.php index 1bce1020cf1..ccf0fdcb02f 100644 --- a/tests/unit/framework/caching/WinCacheTest.php +++ b/tests/unit/framework/caching/WinCacheTest.php @@ -10,24 +10,25 @@ */ class WinCacheTest extends CacheTestCase { - private $_cacheInstance = null; + private $_cacheInstance = null; - /** - * @return WinCache - */ - protected function getCacheInstance() - { - if (!extension_loaded('wincache')) { - $this->markTestSkipped("Wincache not installed. Skipping."); - } + /** + * @return WinCache + */ + protected function getCacheInstance() + { + if (!extension_loaded('wincache')) { + $this->markTestSkipped("Wincache not installed. Skipping."); + } - if (!ini_get('wincache.ucenabled')) { - $this->markTestSkipped("Wincache user cache disabled. Skipping."); - } + if (!ini_get('wincache.ucenabled')) { + $this->markTestSkipped("Wincache user cache disabled. Skipping."); + } - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new WinCache(); - } - return $this->_cacheInstance; - } + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new WinCache(); + } + + return $this->_cacheInstance; + } } diff --git a/tests/unit/framework/caching/XCacheTest.php b/tests/unit/framework/caching/XCacheTest.php index 989765d747f..ca2b79c8b40 100644 --- a/tests/unit/framework/caching/XCacheTest.php +++ b/tests/unit/framework/caching/XCacheTest.php @@ -10,20 +10,21 @@ */ class XCacheTest extends CacheTestCase { - private $_cacheInstance = null; + private $_cacheInstance = null; - /** - * @return XCache - */ - protected function getCacheInstance() - { - if (!function_exists("xcache_isset")) { - $this->markTestSkipped("XCache not installed. Skipping."); - } + /** + * @return XCache + */ + protected function getCacheInstance() + { + if (!function_exists("xcache_isset")) { + $this->markTestSkipped("XCache not installed. Skipping."); + } - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new XCache(); - } - return $this->_cacheInstance; - } + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new XCache(); + } + + return $this->_cacheInstance; + } } diff --git a/tests/unit/framework/caching/ZendDataCacheTest.php b/tests/unit/framework/caching/ZendDataCacheTest.php index 2a0af9fa2de..63c0d10f0f5 100644 --- a/tests/unit/framework/caching/ZendDataCacheTest.php +++ b/tests/unit/framework/caching/ZendDataCacheTest.php @@ -10,20 +10,21 @@ */ class ZendDataCacheTest extends CacheTestCase { - private $_cacheInstance = null; + private $_cacheInstance = null; - /** - * @return ZendDataCache - */ - protected function getCacheInstance() - { - if (!function_exists("zend_shm_cache_store")) { - $this->markTestSkipped("Zend Data cache not installed. Skipping."); - } + /** + * @return ZendDataCache + */ + protected function getCacheInstance() + { + if (!function_exists("zend_shm_cache_store")) { + $this->markTestSkipped("Zend Data cache not installed. Skipping."); + } - if ($this->_cacheInstance === null) { - $this->_cacheInstance = new ZendDataCache(); - } - return $this->_cacheInstance; - } + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new ZendDataCache(); + } + + return $this->_cacheInstance; + } } diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php index 959e744fdbc..53a3cd2001b 100644 --- a/tests/unit/framework/console/controllers/AssetControllerTest.php +++ b/tests/unit/framework/console/controllers/AssetControllerTest.php @@ -15,356 +15,362 @@ */ class AssetControllerTest extends TestCase { - /** - * @var string path for the test files. - */ - protected $testFilePath = ''; - /** - * @var string test assets path. - */ - protected $testAssetsBasePath = ''; - - public function setUp() - { - $this->mockApplication(); - $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); - $this->createDir($this->testFilePath); - $this->testAssetsBasePath = $this->testFilePath . DIRECTORY_SEPARATOR . 'assets'; - $this->createDir($this->testAssetsBasePath); - } - - public function tearDown() - { - $this->removeDir($this->testFilePath); - } - - /** - * Creates directory. - * @param string $dirName directory full name. - */ - protected function createDir($dirName) - { - if (!file_exists($dirName)) { - mkdir($dirName, 0777, true); - } - } - - /** - * Removes directory. - * @param string $dirName directory full name - */ - protected function removeDir($dirName) - { - if (!empty($dirName) && file_exists($dirName)) { - exec("rm -rf {$dirName}"); - } - } - - /** - * Creates test asset controller instance. - * @return AssetController - */ - protected function createAssetController() - { - $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); - $assetController = new AssetController('asset', $module); - $assetController->interactive = false; - $assetController->jsCompressor = 'cp {from} {to}'; - $assetController->cssCompressor = 'cp {from} {to}'; - return $assetController; - } - - /** - * Emulates running of the asset controller action. - * @param string $actionId id of action to be run. - * @param array $args action arguments. - * @return string command output. - */ - protected function runAssetControllerAction($actionId, array $args = []) - { - $controller = $this->createAssetController(); - ob_start(); - ob_implicit_flush(false); - $controller->run($actionId, $args); - return ob_get_clean(); - } - - /** - * Creates test compress config. - * @param array[] $bundles asset bundles config. - * @return array config array. - */ - protected function createCompressConfig(array $bundles) - { - $className = $this->declareAssetBundleClass(['class' => 'AssetBundleAll']); - $baseUrl = '/test'; - $config = [ - 'bundles' => $bundles, - 'targets' => [ - $className => [ - 'basePath' => $this->testAssetsBasePath, - 'baseUrl' => $baseUrl, - 'js' => 'all.js', - 'css' => 'all.css', - ], - ], - 'assetManager' => [ - 'basePath' => $this->testAssetsBasePath, - 'baseUrl' => '', - ], - ]; - return $config; - } - - /** - * Creates test compress config file. - * @param string $fileName output file name. - * @param array[] $bundles asset bundles config. - * @throws \Exception on failure. - */ - protected function createCompressConfigFile($fileName, array $bundles) - { - $content = 'createCompressConfig($bundles), true) . ';'; - if (file_put_contents($fileName, $content) <= 0) { - throw new \Exception("Unable to create file '{$fileName}'!"); - } - } - - /** - * Creates test asset file. - * @param string $fileRelativeName file name relative to [[testFilePath]] - * @param string $content file content - * @throws \Exception on failure. - */ - protected function createAssetSourceFile($fileRelativeName, $content) - { - $fileFullName = $this->testFilePath . DIRECTORY_SEPARATOR . $fileRelativeName; - $this->createDir(dirname($fileFullName)); - if (file_put_contents($fileFullName, $content) <= 0) { - throw new \Exception("Unable to create file '{$fileFullName}'!"); - } - } - - /** - * Creates a list of asset source files. - * @param array $files assert source files in format: file/relative/name => fileContent - */ - protected function createAssetSourceFiles(array $files) - { - foreach ($files as $name => $content) { - $this->createAssetSourceFile($name, $content); - } - } - - /** - * Invokes the asset controller method even if it is protected. - * @param string $methodName name of the method to be invoked. - * @param array $args method arguments. - * @return mixed method invoke result. - */ - protected function invokeAssetControllerMethod($methodName, array $args = []) - { - $controller = $this->createAssetController(); - $controllerClassReflection = new \ReflectionClass(get_class($controller)); - $methodReflection = $controllerClassReflection->getMethod($methodName); - $methodReflection->setAccessible(true); - $result = $methodReflection->invokeArgs($controller, $args); - $methodReflection->setAccessible(false); - return $result; - } - - /** - * Composes asset bundle class source code. - * @param array $config asset bundle config. - * @return string class source code. - */ - protected function composeAssetBundleClassSource(array &$config) - { - $config = array_merge( - [ - 'namespace' => StringHelper::dirname(get_class($this)), - 'class' => 'AppAsset', - 'basePath' => $this->testFilePath, - 'baseUrl' => '', - 'css' => [], - 'js' => [], - 'depends' => [], - ], - $config - ); - foreach ($config as $name => $value) { - if (is_array($value)) { - $config[$name] = var_export($value, true); - } - } - - $source = <<mockApplication(); + $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); + $this->createDir($this->testFilePath); + $this->testAssetsBasePath = $this->testFilePath . DIRECTORY_SEPARATOR . 'assets'; + $this->createDir($this->testAssetsBasePath); + } + + public function tearDown() + { + $this->removeDir($this->testFilePath); + } + + /** + * Creates directory. + * @param string $dirName directory full name. + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param string $dirName directory full name + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && file_exists($dirName)) { + exec("rm -rf {$dirName}"); + } + } + + /** + * Creates test asset controller instance. + * @return AssetController + */ + protected function createAssetController() + { + $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); + $assetController = new AssetController('asset', $module); + $assetController->interactive = false; + $assetController->jsCompressor = 'cp {from} {to}'; + $assetController->cssCompressor = 'cp {from} {to}'; + + return $assetController; + } + + /** + * Emulates running of the asset controller action. + * @param string $actionId id of action to be run. + * @param array $args action arguments. + * @return string command output. + */ + protected function runAssetControllerAction($actionId, array $args = []) + { + $controller = $this->createAssetController(); + ob_start(); + ob_implicit_flush(false); + $controller->run($actionId, $args); + + return ob_get_clean(); + } + + /** + * Creates test compress config. + * @param array[] $bundles asset bundles config. + * @return array config array. + */ + protected function createCompressConfig(array $bundles) + { + $className = $this->declareAssetBundleClass(['class' => 'AssetBundleAll']); + $baseUrl = '/test'; + $config = [ + 'bundles' => $bundles, + 'targets' => [ + $className => [ + 'basePath' => $this->testAssetsBasePath, + 'baseUrl' => $baseUrl, + 'js' => 'all.js', + 'css' => 'all.css', + ], + ], + 'assetManager' => [ + 'basePath' => $this->testAssetsBasePath, + 'baseUrl' => '', + ], + ]; + + return $config; + } + + /** + * Creates test compress config file. + * @param string $fileName output file name. + * @param array[] $bundles asset bundles config. + * @throws \Exception on failure. + */ + protected function createCompressConfigFile($fileName, array $bundles) + { + $content = 'createCompressConfig($bundles), true) . ';'; + if (file_put_contents($fileName, $content) <= 0) { + throw new \Exception("Unable to create file '{$fileName}'!"); + } + } + + /** + * Creates test asset file. + * @param string $fileRelativeName file name relative to [[testFilePath]] + * @param string $content file content + * @throws \Exception on failure. + */ + protected function createAssetSourceFile($fileRelativeName, $content) + { + $fileFullName = $this->testFilePath . DIRECTORY_SEPARATOR . $fileRelativeName; + $this->createDir(dirname($fileFullName)); + if (file_put_contents($fileFullName, $content) <= 0) { + throw new \Exception("Unable to create file '{$fileFullName}'!"); + } + } + + /** + * Creates a list of asset source files. + * @param array $files assert source files in format: file/relative/name => fileContent + */ + protected function createAssetSourceFiles(array $files) + { + foreach ($files as $name => $content) { + $this->createAssetSourceFile($name, $content); + } + } + + /** + * Invokes the asset controller method even if it is protected. + * @param string $methodName name of the method to be invoked. + * @param array $args method arguments. + * @return mixed method invoke result. + */ + protected function invokeAssetControllerMethod($methodName, array $args = []) + { + $controller = $this->createAssetController(); + $controllerClassReflection = new \ReflectionClass(get_class($controller)); + $methodReflection = $controllerClassReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($controller, $args); + $methodReflection->setAccessible(false); + + return $result; + } + + /** + * Composes asset bundle class source code. + * @param array $config asset bundle config. + * @return string class source code. + */ + protected function composeAssetBundleClassSource(array &$config) + { + $config = array_merge( + [ + 'namespace' => StringHelper::dirname(get_class($this)), + 'class' => 'AppAsset', + 'basePath' => $this->testFilePath, + 'baseUrl' => '', + 'css' => [], + 'js' => [], + 'depends' => [], + ], + $config + ); + foreach ($config as $name => $value) { + if (is_array($value)) { + $config[$name] = var_export($value, true); + } + } + + $source = <<composeAssetBundleClassSource($config); - eval($sourceCode); - return $config['namespace'] . '\\' . $config['class']; - } - - // Tests : - - public function testActionTemplate() - { - $configFileName = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; - $this->runAssetControllerAction('template', [$configFileName]); - $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); - } - - public function testActionCompress() - { - // Given : - $cssFiles = [ - 'css/test_body.css' => 'body { - padding-top: 20px; - padding-bottom: 60px; - }', - 'css/test_footer.css' => '.footer { - margin: 20px; - display: block; - }', - ]; - $this->createAssetSourceFiles($cssFiles); - - $jsFiles = [ - 'js/test_alert.js' => "function test() { - alert('Test message'); - }", - 'js/test_sum_ab.js' => "function sumAB(a, b) { - return a + b; - }", - ]; - $this->createAssetSourceFiles($jsFiles); - $assetBundleClassName = $this->declareAssetBundleClass([ - 'css' => array_keys($cssFiles), - 'js' => array_keys($jsFiles), - ]); - - $bundles = [ - $assetBundleClassName - ]; - $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; - - $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; - $this->createCompressConfigFile($configFile, $bundles); - - // When : - $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); - - // Then : - $this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!'); - $this->assertTrue(is_array(require($bundleFile)), 'Output bundle file has incorrect format!'); - - $compressedCssFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.css'; - $this->assertTrue(file_exists($compressedCssFileName), 'Unable to compress CSS files!'); - $compressedJsFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.js'; - $this->assertTrue(file_exists($compressedJsFileName), 'Unable to compress JS files!'); - - $compressedCssFileContent = file_get_contents($compressedCssFileName); - foreach ($cssFiles as $name => $content) { - $this->assertContains($content, $compressedCssFileContent, "Source of '{$name}' is missing in combined file!"); - } - $compressedJsFileContent = file_get_contents($compressedJsFileName); - foreach ($jsFiles as $name => $content) { - $this->assertContains($content, $compressedJsFileContent, "Source of '{$name}' is missing in combined file!"); - } - } - - /** - * Data provider for [[testAdjustCssUrl()]]. - * @return array test data. - */ - public function adjustCssUrlDataProvider() - { - return [ - [ - '.published-same-dir-class {background-image: url(published_same_dir.png);}', - '/test/base/path/assets/input', - '/test/base/path/assets/output', - '.published-same-dir-class {background-image: url(../input/published_same_dir.png);}', - ], - [ - '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', - '/test/base/path/assets/input', - '/test/base/path/assets/output', - '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', - ], - [ - '.static-same-dir-class {background-image: url(\'static_same_dir.png\');}', - '/test/base/path/css', - '/test/base/path/assets/output', - '.static-same-dir-class {background-image: url(\'../../css/static_same_dir.png\');}', - ], - [ - '.static-relative-dir-class {background-image: url("../img/static_relative_dir.png");}', - '/test/base/path/css', - '/test/base/path/assets/output', - '.static-relative-dir-class {background-image: url("../../img/static_relative_dir.png");}', - ], - [ - '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', - '/test/base/path/assets/input', - '/test/base/path/assets/output', - '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', - ], - [ - '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', - '/test/base/path/assets/input', - '/test/base/path/assets/output', - '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', - ], - [ - "@font-face { - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); - }", - '/test/base/path/assets/input/css', - '/test/base/path/assets/output', - "@font-face { - src: url('../input/fonts/glyphicons-halflings-regular.eot'); - src: url('../input/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); - }", - ], - ]; - } - - /** - * @dataProvider adjustCssUrlDataProvider - * - * @param $cssContent - * @param $inputFilePath - * @param $outputFilePath - * @param $expectedCssContent - */ - public function testAdjustCssUrl($cssContent, $inputFilePath, $outputFilePath, $expectedCssContent) - { - $adjustedCssContent = $this->invokeAssetControllerMethod('adjustCssUrl', [$cssContent, $inputFilePath, $outputFilePath]); - - $this->assertEquals($expectedCssContent, $adjustedCssContent, 'Unable to adjust CSS correctly!'); - } + + return $source; + } + + /** + * Declares asset bundle class according to given configuration. + * @param array $config asset bundle config. + * @return string new class full name. + */ + protected function declareAssetBundleClass(array $config) + { + $sourceCode = $this->composeAssetBundleClassSource($config); + eval($sourceCode); + + return $config['namespace'] . '\\' . $config['class']; + } + + // Tests : + + public function testActionTemplate() + { + $configFileName = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; + $this->runAssetControllerAction('template', [$configFileName]); + $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); + } + + public function testActionCompress() + { + // Given : + $cssFiles = [ + 'css/test_body.css' => 'body { + padding-top: 20px; + padding-bottom: 60px; + }', + 'css/test_footer.css' => '.footer { + margin: 20px; + display: block; + }', + ]; + $this->createAssetSourceFiles($cssFiles); + + $jsFiles = [ + 'js/test_alert.js' => "function test() { + alert('Test message'); + }", + 'js/test_sum_ab.js' => "function sumAB(a, b) { + return a + b; + }", + ]; + $this->createAssetSourceFiles($jsFiles); + $assetBundleClassName = $this->declareAssetBundleClass([ + 'css' => array_keys($cssFiles), + 'js' => array_keys($jsFiles), + ]); + + $bundles = [ + $assetBundleClassName + ]; + $bundleFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'bundle.php'; + + $configFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'config.php'; + $this->createCompressConfigFile($configFile, $bundles); + + // When : + $this->runAssetControllerAction('compress', [$configFile, $bundleFile]); + + // Then : + $this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!'); + $this->assertTrue(is_array(require($bundleFile)), 'Output bundle file has incorrect format!'); + + $compressedCssFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.css'; + $this->assertTrue(file_exists($compressedCssFileName), 'Unable to compress CSS files!'); + $compressedJsFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.js'; + $this->assertTrue(file_exists($compressedJsFileName), 'Unable to compress JS files!'); + + $compressedCssFileContent = file_get_contents($compressedCssFileName); + foreach ($cssFiles as $name => $content) { + $this->assertContains($content, $compressedCssFileContent, "Source of '{$name}' is missing in combined file!"); + } + $compressedJsFileContent = file_get_contents($compressedJsFileName); + foreach ($jsFiles as $name => $content) { + $this->assertContains($content, $compressedJsFileContent, "Source of '{$name}' is missing in combined file!"); + } + } + + /** + * Data provider for [[testAdjustCssUrl()]]. + * @return array test data. + */ + public function adjustCssUrlDataProvider() + { + return [ + [ + '.published-same-dir-class {background-image: url(published_same_dir.png);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.published-same-dir-class {background-image: url(../input/published_same_dir.png);}', + ], + [ + '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.published-relative-dir-class {background-image: url(../img/published_relative_dir.png);}', + ], + [ + '.static-same-dir-class {background-image: url(\'static_same_dir.png\');}', + '/test/base/path/css', + '/test/base/path/assets/output', + '.static-same-dir-class {background-image: url(\'../../css/static_same_dir.png\');}', + ], + [ + '.static-relative-dir-class {background-image: url("../img/static_relative_dir.png");}', + '/test/base/path/css', + '/test/base/path/assets/output', + '.static-relative-dir-class {background-image: url("../../img/static_relative_dir.png");}', + ], + [ + '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.absolute-url-class {background-image: url(http://domain.com/img/image.gif);}', + ], + [ + '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', + '/test/base/path/assets/input', + '/test/base/path/assets/output', + '.absolute-url-secure-class {background-image: url(https://secure.domain.com/img/image.gif);}', + ], + [ + "@font-face { + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); + }", + '/test/base/path/assets/input/css', + '/test/base/path/assets/output', + "@font-face { + src: url('../input/fonts/glyphicons-halflings-regular.eot'); + src: url('../input/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'); + }", + ], + ]; + } + + /** + * @dataProvider adjustCssUrlDataProvider + * + * @param $cssContent + * @param $inputFilePath + * @param $outputFilePath + * @param $expectedCssContent + */ + public function testAdjustCssUrl($cssContent, $inputFilePath, $outputFilePath, $expectedCssContent) + { + $adjustedCssContent = $this->invokeAssetControllerMethod('adjustCssUrl', [$cssContent, $inputFilePath, $outputFilePath]); + + $this->assertEquals($expectedCssContent, $adjustedCssContent, 'Unable to adjust CSS correctly!'); + } } diff --git a/tests/unit/framework/console/controllers/MessageControllerTest.php b/tests/unit/framework/console/controllers/MessageControllerTest.php index 493fb32a498..aa2bfd2eb4c 100644 --- a/tests/unit/framework/console/controllers/MessageControllerTest.php +++ b/tests/unit/framework/console/controllers/MessageControllerTest.php @@ -11,359 +11,361 @@ */ class MessageControllerTest extends TestCase { - protected $sourcePath = ''; - protected $messagePath = ''; - protected $configFileName = ''; - - public function setUp() - { - $this->mockApplication(); - $this->sourcePath = Yii::getAlias('@yiiunit/runtime/test_source'); - $this->createDir($this->sourcePath); - if (!file_exists($this->sourcePath)) { - $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); - } - $this->messagePath = Yii::getAlias('@yiiunit/runtime/test_messages'); - $this->createDir($this->messagePath); - $this->configFileName = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . 'message_controller_test_config.php'; - } - - public function tearDown() - { - $this->removeDir($this->sourcePath); - $this->removeDir($this->messagePath); - if (file_exists($this->configFileName)) { - unlink($this->configFileName); - } - } - - /** - * Creates directory. - * @param $dirName directory full name - */ - protected function createDir($dirName) - { - if (!file_exists($dirName)) { - mkdir($dirName, 0777, true); - } - } - - /** - * Removes directory. - * @param $dirName directory full name - */ - protected function removeDir($dirName) - { - if (!empty($dirName) && file_exists($dirName)) { - $this->removeFileSystemObject($dirName); - } - } - - /** - * Removes file system object: directory or file. - * @param string $fileSystemObjectFullName file system object full name. - */ - protected function removeFileSystemObject($fileSystemObjectFullName) - { - if (!is_dir($fileSystemObjectFullName)) { - unlink($fileSystemObjectFullName); - } else { - $dirHandle = opendir($fileSystemObjectFullName); - while (($fileSystemObjectName = readdir($dirHandle)) !== false) { - if ($fileSystemObjectName === '.' || $fileSystemObjectName === '..') { - continue; - } - $this->removeFileSystemObject($fileSystemObjectFullName . DIRECTORY_SEPARATOR . $fileSystemObjectName); - } - closedir($dirHandle); - rmdir($fileSystemObjectFullName); - } - } - - /** - * Creates test message controller instance. - * @return MessageController message command instance. - */ - protected function createMessageController() - { - $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); - $messageController = new MessageController('message', $module); - $messageController->interactive = false; - return $messageController; - } - - /** - * Emulates running of the message controller action. - * @param string $actionId id of action to be run. - * @param array $args action arguments. - * @return string command output. - */ - protected function runMessageControllerAction($actionId, array $args = []) - { - $controller = $this->createMessageController(); - ob_start(); - ob_implicit_flush(false); - $controller->run($actionId, $args); - return ob_get_clean(); - } - - /** - * Creates message command config file named as [[configFileName]]. - * @param array $config message command config. - */ - protected function composeConfigFile(array $config) - { - if (file_exists($this->configFileName)) { - unlink($this->configFileName); - } - $fileContent = 'configFileName, $fileContent); - } - - /** - * Creates source file with given content - * @param string $content file content - * @param string|null $name file self name - */ - protected function createSourceFile($content, $name = null) - { - if (empty($name)) { - $name = md5(uniqid()) . '.php'; - } - file_put_contents($this->sourcePath . DIRECTORY_SEPARATOR . $name, $content); - } - - /** - * Creates message file with given messages. - * @param string $name file name - * @param array $messages messages. - */ - protected function createMessageFile($name, array $messages = []) - { - $fileName = $this->messagePath . DIRECTORY_SEPARATOR . $name; - if (file_exists($fileName)) { - unlink($fileName); - } else { - $dirName = dirname($fileName); - if (!file_exists($dirName)) { - mkdir($dirName, 0777, true); - } - } - $fileContent = 'configFileName; - $this->runMessageControllerAction('config', [$configFileName]); - $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); - } - - public function testConfigFileNotExist() - { - $this->setExpectedException('yii\\console\\Exception'); - $this->runMessageControllerAction('extract', ['not_existing_file.php']); - } - - public function testCreateTranslation() - { - $language = 'en'; - - $category = 'test_category'; - $message = 'test message'; - $sourceFileContent = "Yii::t('{$category}', '{$message}')"; - $this->createSourceFile($sourceFileContent); - - $this->composeConfigFile([ - 'languages' => [$language], - 'sourcePath' => $this->sourcePath, - 'messagePath' => $this->messagePath, - ]); - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $this->assertTrue(file_exists($this->messagePath . DIRECTORY_SEPARATOR . $language), 'No language dir created!'); - $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; - $this->assertTrue(file_exists($messageFileName), 'No message file created!'); - $messages = require($messageFileName); - $this->assertTrue(is_array($messages), 'Unable to compose messages!'); - $this->assertTrue(array_key_exists($message, $messages), 'Source message is missing!'); - } - - /** - * @depends testCreateTranslation - */ - public function testNothingNew() - { - $language = 'en'; - - $category = 'test_category'; - $message = 'test message'; - $sourceFileContent = "Yii::t('{$category}', '{$message}')"; - $this->createSourceFile($sourceFileContent); - - $this->composeConfigFile([ - 'languages' => [$language], - 'sourcePath' => $this->sourcePath, - 'messagePath' => $this->messagePath, - ]); - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; - - // check file not overwritten: - $messageFileContent = file_get_contents($messageFileName); - $messageFileContent .= '// some not generated by command content'; - file_put_contents($messageFileName, $messageFileContent); - - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $this->assertEquals($messageFileContent, file_get_contents($messageFileName)); - } - - /** - * @depends testCreateTranslation - */ - public function testMerge() - { - $language = 'en'; - $category = 'test_category'; - $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; - - $existingMessage = 'test existing message'; - $existingMessageContent = 'test existing message content'; - $this->createMessageFile($messageFileName, [ - $existingMessage => $existingMessageContent - ]); - - $newMessage = 'test new message'; - $sourceFileContent = "Yii::t('{$category}', '{$existingMessage}')"; - $sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')"; - $this->createSourceFile($sourceFileContent); - - $this->composeConfigFile([ - 'languages' => [$language], - 'sourcePath' => $this->sourcePath, - 'messagePath' => $this->messagePath, - 'overwrite' => true, - ]); - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); - $this->assertTrue(array_key_exists($newMessage, $messages), 'Unable to add new message!'); - $this->assertTrue(array_key_exists($existingMessage, $messages), 'Unable to keep existing message!'); - $this->assertEquals('', $messages[$newMessage], 'Wrong new message content!'); - $this->assertEquals($existingMessageContent, $messages[$existingMessage], 'Unable to keep existing message content!'); - } - - /** - * @depends testMerge - */ - public function testNoLongerNeedTranslation() - { - $language = 'en'; - $category = 'test_category'; - $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; - - $oldMessage = 'test old message'; - $oldMessageContent = 'test old message content'; - $this->createMessageFile($messageFileName, [ - $oldMessage => $oldMessageContent - ]); - - $sourceFileContent = "Yii::t('{$category}', 'some new message')"; - $this->createSourceFile($sourceFileContent); - - $this->composeConfigFile([ - 'languages' => [$language], - 'sourcePath' => $this->sourcePath, - 'messagePath' => $this->messagePath, - 'overwrite' => true, - 'removeUnused' => false, - ]); - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); - - $this->assertTrue(array_key_exists($oldMessage, $messages), 'No longer needed message removed!'); - $this->assertEquals('@@' . $oldMessageContent . '@@', $messages[$oldMessage], 'No longer needed message content does not marked properly!'); - } - - /** - * @depends testMerge - */ - public function testMergeWithContentZero() - { - $language = 'en'; - $category = 'test_category'; - $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; - - $zeroMessage = 'test zero message'; - $zeroMessageContent = '0'; - $falseMessage = 'test false message'; - $falseMessageContent = 'false'; - $this->createMessageFile($messageFileName, [ - $zeroMessage => $zeroMessageContent, - $falseMessage => $falseMessageContent, - ]); - - $newMessage = 'test new message'; - $sourceFileContent = "Yii::t('{$category}', '{$zeroMessage}')"; - $sourceFileContent .= "Yii::t('{$category}', '{$falseMessage}')"; - $sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')"; - $this->createSourceFile($sourceFileContent); - - $this->composeConfigFile([ - 'languages' => [$language], - 'sourcePath' => $this->sourcePath, - 'messagePath' => $this->messagePath, - 'overwrite' => true, - ]); - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); - $this->assertTrue($zeroMessageContent === $messages[$zeroMessage], 'Message content "0" is lost!'); - $this->assertTrue($falseMessageContent === $messages[$falseMessage], 'Message content "false" is lost!'); - } - - /** - * @depends testCreateTranslation - */ - public function testMultiplyTranslators() - { - $language = 'en'; - $category = 'test_category'; - - $translators = [ - 'Yii::t', - 'Custom::translate', - ]; - - $sourceMessages = [ - 'first message', - 'second message', - ]; - $sourceFileContent = ''; - foreach ($sourceMessages as $key => $message) { - $sourceFileContent .= $translators[$key] . "('{$category}', '{$message}');\n"; - } - $this->createSourceFile($sourceFileContent); - - $this->composeConfigFile([ - 'languages' => [$language], - 'sourcePath' => $this->sourcePath, - 'messagePath' => $this->messagePath, - 'translator' => $translators, - ]); - $this->runMessageControllerAction('extract', [$this->configFileName]); - - $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; - $messages = require($messageFileName); - - foreach ($sourceMessages as $sourceMessage) { - $this->assertTrue(array_key_exists($sourceMessage, $messages)); - } - } + protected $sourcePath = ''; + protected $messagePath = ''; + protected $configFileName = ''; + + public function setUp() + { + $this->mockApplication(); + $this->sourcePath = Yii::getAlias('@yiiunit/runtime/test_source'); + $this->createDir($this->sourcePath); + if (!file_exists($this->sourcePath)) { + $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); + } + $this->messagePath = Yii::getAlias('@yiiunit/runtime/test_messages'); + $this->createDir($this->messagePath); + $this->configFileName = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . 'message_controller_test_config.php'; + } + + public function tearDown() + { + $this->removeDir($this->sourcePath); + $this->removeDir($this->messagePath); + if (file_exists($this->configFileName)) { + unlink($this->configFileName); + } + } + + /** + * Creates directory. + * @param $dirName directory full name + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param $dirName directory full name + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && file_exists($dirName)) { + $this->removeFileSystemObject($dirName); + } + } + + /** + * Removes file system object: directory or file. + * @param string $fileSystemObjectFullName file system object full name. + */ + protected function removeFileSystemObject($fileSystemObjectFullName) + { + if (!is_dir($fileSystemObjectFullName)) { + unlink($fileSystemObjectFullName); + } else { + $dirHandle = opendir($fileSystemObjectFullName); + while (($fileSystemObjectName = readdir($dirHandle)) !== false) { + if ($fileSystemObjectName === '.' || $fileSystemObjectName === '..') { + continue; + } + $this->removeFileSystemObject($fileSystemObjectFullName . DIRECTORY_SEPARATOR . $fileSystemObjectName); + } + closedir($dirHandle); + rmdir($fileSystemObjectFullName); + } + } + + /** + * Creates test message controller instance. + * @return MessageController message command instance. + */ + protected function createMessageController() + { + $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); + $messageController = new MessageController('message', $module); + $messageController->interactive = false; + + return $messageController; + } + + /** + * Emulates running of the message controller action. + * @param string $actionId id of action to be run. + * @param array $args action arguments. + * @return string command output. + */ + protected function runMessageControllerAction($actionId, array $args = []) + { + $controller = $this->createMessageController(); + ob_start(); + ob_implicit_flush(false); + $controller->run($actionId, $args); + + return ob_get_clean(); + } + + /** + * Creates message command config file named as [[configFileName]]. + * @param array $config message command config. + */ + protected function composeConfigFile(array $config) + { + if (file_exists($this->configFileName)) { + unlink($this->configFileName); + } + $fileContent = 'configFileName, $fileContent); + } + + /** + * Creates source file with given content + * @param string $content file content + * @param string|null $name file self name + */ + protected function createSourceFile($content, $name = null) + { + if (empty($name)) { + $name = md5(uniqid()) . '.php'; + } + file_put_contents($this->sourcePath . DIRECTORY_SEPARATOR . $name, $content); + } + + /** + * Creates message file with given messages. + * @param string $name file name + * @param array $messages messages. + */ + protected function createMessageFile($name, array $messages = []) + { + $fileName = $this->messagePath . DIRECTORY_SEPARATOR . $name; + if (file_exists($fileName)) { + unlink($fileName); + } else { + $dirName = dirname($fileName); + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + $fileContent = 'configFileName; + $this->runMessageControllerAction('config', [$configFileName]); + $this->assertTrue(file_exists($configFileName), 'Unable to create config file template!'); + } + + public function testConfigFileNotExist() + { + $this->setExpectedException('yii\\console\\Exception'); + $this->runMessageControllerAction('extract', ['not_existing_file.php']); + } + + public function testCreateTranslation() + { + $language = 'en'; + + $category = 'test_category'; + $message = 'test message'; + $sourceFileContent = "Yii::t('{$category}', '{$message}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile([ + 'languages' => [$language], + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + ]); + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $this->assertTrue(file_exists($this->messagePath . DIRECTORY_SEPARATOR . $language), 'No language dir created!'); + $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; + $this->assertTrue(file_exists($messageFileName), 'No message file created!'); + $messages = require($messageFileName); + $this->assertTrue(is_array($messages), 'Unable to compose messages!'); + $this->assertTrue(array_key_exists($message, $messages), 'Source message is missing!'); + } + + /** + * @depends testCreateTranslation + */ + public function testNothingNew() + { + $language = 'en'; + + $category = 'test_category'; + $message = 'test message'; + $sourceFileContent = "Yii::t('{$category}', '{$message}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile([ + 'languages' => [$language], + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + ]); + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; + + // check file not overwritten: + $messageFileContent = file_get_contents($messageFileName); + $messageFileContent .= '// some not generated by command content'; + file_put_contents($messageFileName, $messageFileContent); + + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $this->assertEquals($messageFileContent, file_get_contents($messageFileName)); + } + + /** + * @depends testCreateTranslation + */ + public function testMerge() + { + $language = 'en'; + $category = 'test_category'; + $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; + + $existingMessage = 'test existing message'; + $existingMessageContent = 'test existing message content'; + $this->createMessageFile($messageFileName, [ + $existingMessage => $existingMessageContent + ]); + + $newMessage = 'test new message'; + $sourceFileContent = "Yii::t('{$category}', '{$existingMessage}')"; + $sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile([ + 'languages' => [$language], + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'overwrite' => true, + ]); + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); + $this->assertTrue(array_key_exists($newMessage, $messages), 'Unable to add new message!'); + $this->assertTrue(array_key_exists($existingMessage, $messages), 'Unable to keep existing message!'); + $this->assertEquals('', $messages[$newMessage], 'Wrong new message content!'); + $this->assertEquals($existingMessageContent, $messages[$existingMessage], 'Unable to keep existing message content!'); + } + + /** + * @depends testMerge + */ + public function testNoLongerNeedTranslation() + { + $language = 'en'; + $category = 'test_category'; + $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; + + $oldMessage = 'test old message'; + $oldMessageContent = 'test old message content'; + $this->createMessageFile($messageFileName, [ + $oldMessage => $oldMessageContent + ]); + + $sourceFileContent = "Yii::t('{$category}', 'some new message')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile([ + 'languages' => [$language], + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'overwrite' => true, + 'removeUnused' => false, + ]); + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); + + $this->assertTrue(array_key_exists($oldMessage, $messages), 'No longer needed message removed!'); + $this->assertEquals('@@' . $oldMessageContent . '@@', $messages[$oldMessage], 'No longer needed message content does not marked properly!'); + } + + /** + * @depends testMerge + */ + public function testMergeWithContentZero() + { + $language = 'en'; + $category = 'test_category'; + $messageFileName = $language . DIRECTORY_SEPARATOR . $category . '.php'; + + $zeroMessage = 'test zero message'; + $zeroMessageContent = '0'; + $falseMessage = 'test false message'; + $falseMessageContent = 'false'; + $this->createMessageFile($messageFileName, [ + $zeroMessage => $zeroMessageContent, + $falseMessage => $falseMessageContent, + ]); + + $newMessage = 'test new message'; + $sourceFileContent = "Yii::t('{$category}', '{$zeroMessage}')"; + $sourceFileContent .= "Yii::t('{$category}', '{$falseMessage}')"; + $sourceFileContent .= "Yii::t('{$category}', '{$newMessage}')"; + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile([ + 'languages' => [$language], + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'overwrite' => true, + ]); + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $messages = require($this->messagePath . DIRECTORY_SEPARATOR . $messageFileName); + $this->assertTrue($zeroMessageContent === $messages[$zeroMessage], 'Message content "0" is lost!'); + $this->assertTrue($falseMessageContent === $messages[$falseMessage], 'Message content "false" is lost!'); + } + + /** + * @depends testCreateTranslation + */ + public function testMultiplyTranslators() + { + $language = 'en'; + $category = 'test_category'; + + $translators = [ + 'Yii::t', + 'Custom::translate', + ]; + + $sourceMessages = [ + 'first message', + 'second message', + ]; + $sourceFileContent = ''; + foreach ($sourceMessages as $key => $message) { + $sourceFileContent .= $translators[$key] . "('{$category}', '{$message}');\n"; + } + $this->createSourceFile($sourceFileContent); + + $this->composeConfigFile([ + 'languages' => [$language], + 'sourcePath' => $this->sourcePath, + 'messagePath' => $this->messagePath, + 'translator' => $translators, + ]); + $this->runMessageControllerAction('extract', [$this->configFileName]); + + $messageFileName = $this->messagePath . DIRECTORY_SEPARATOR . $language . DIRECTORY_SEPARATOR . $category . '.php'; + $messages = require($messageFileName); + + foreach ($sourceMessages as $sourceMessage) { + $this->assertTrue(array_key_exists($sourceMessage, $messages)); + } + } } diff --git a/tests/unit/framework/data/ActiveDataProviderTest.php b/tests/unit/framework/data/ActiveDataProviderTest.php index a58deabd286..75e05bff92f 100644 --- a/tests/unit/framework/data/ActiveDataProviderTest.php +++ b/tests/unit/framework/data/ActiveDataProviderTest.php @@ -24,157 +24,157 @@ */ class ActiveDataProviderTest extends DatabaseTestCase { - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - } - - public function testActiveQuery() - { - $provider = new ActiveDataProvider([ - 'query' => Order::find()->orderBy('id'), - ]); - $orders = $provider->getModels(); - $this->assertEquals(3, count($orders)); - $this->assertTrue($orders[0] instanceof Order); - $this->assertTrue($orders[1] instanceof Order); - $this->assertTrue($orders[2] instanceof Order); - $this->assertEquals([1, 2, 3], $provider->getKeys()); - - $provider = new ActiveDataProvider([ - 'query' => Order::find(), - 'pagination' => [ - 'pageSize' => 2, - ] - ]); - $orders = $provider->getModels(); - $this->assertEquals(2, count($orders)); - } - - public function testActiveRelation() - { - /** @var Customer $customer */ - $customer = Customer::find(2); - $provider = new ActiveDataProvider([ - 'query' => $customer->getOrders(), - ]); - $orders = $provider->getModels(); - $this->assertEquals(2, count($orders)); - $this->assertTrue($orders[0] instanceof Order); - $this->assertTrue($orders[1] instanceof Order); - $this->assertEquals([2, 3], $provider->getKeys()); - - $provider = new ActiveDataProvider([ - 'query' => $customer->getOrders(), - 'pagination' => [ - 'pageSize' => 1, - ] - ]); - $orders = $provider->getModels(); - $this->assertEquals(1, count($orders)); - } - - public function testActiveRelationVia() - { - /** @var Order $order */ - $order = Order::find(2); - $provider = new ActiveDataProvider([ - 'query' => $order->getItems(), - ]); - $items = $provider->getModels(); - $this->assertEquals(3, count($items)); - $this->assertTrue($items[0] instanceof Item); - $this->assertTrue($items[1] instanceof Item); - $this->assertTrue($items[2] instanceof Item); - $this->assertEquals([3, 4, 5], $provider->getKeys()); - - $provider = new ActiveDataProvider([ - 'query' => $order->getItems(), - 'pagination' => [ - 'pageSize' => 2, - ] - ]); - $items = $provider->getModels(); - $this->assertEquals(2, count($items)); - } - - public function testActiveRelationViaTable() - { - /** @var Order $order */ - $order = Order::find(1); - $provider = new ActiveDataProvider([ - 'query' => $order->getBooks(), - ]); - $items = $provider->getModels(); - $this->assertEquals(2, count($items)); - $this->assertTrue($items[0] instanceof Item); - $this->assertTrue($items[1] instanceof Item); - - $provider = new ActiveDataProvider([ - 'query' => $order->getBooks(), - 'pagination' => [ - 'pageSize' => 1, - ] - ]); - $items = $provider->getModels(); - $this->assertEquals(1, count($items)); - } - - public function testQuery() - { - $query = new Query; - $provider = new ActiveDataProvider([ - 'db' => $this->getConnection(), - 'query' => $query->from('tbl_order')->orderBy('id'), - ]); - $orders = $provider->getModels(); - $this->assertEquals(3, count($orders)); - $this->assertTrue(is_array($orders[0])); - $this->assertEquals([0, 1, 2], $provider->getKeys()); - - $query = new Query; - $provider = new ActiveDataProvider([ - 'db' => $this->getConnection(), - 'query' => $query->from('tbl_order'), - 'pagination' => [ - 'pageSize' => 2, - ] - ]); - $orders = $provider->getModels(); - $this->assertEquals(2, count($orders)); - } - - public function testRefresh() - { - $query = new Query; - $provider = new ActiveDataProvider([ - 'db' => $this->getConnection(), - 'query' => $query->from('tbl_order')->orderBy('id'), - ]); - $this->assertEquals(3, count($provider->getModels())); - - $provider->getPagination()->pageSize = 2; - $this->assertEquals(3, count($provider->getModels())); - $provider->refresh(); - $this->assertEquals(2, count($provider->getModels())); - } - - public function testPaginationBeforeModels() - { - $query = new Query; - $provider = new ActiveDataProvider([ - 'db' => $this->getConnection(), - 'query' => $query->from('tbl_order')->orderBy('id'), - ]); - $pagination = $provider->getPagination(); - $this->assertEquals(0, $pagination->getPageCount()); - $this->assertCount(3, $provider->getModels()); - $this->assertEquals(1, $pagination->getPageCount()); - - $provider->getPagination()->pageSize = 2; - $this->assertEquals(3, count($provider->getModels())); - $provider->refresh(); - $this->assertEquals(2, count($provider->getModels())); - } + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } + + public function testActiveQuery() + { + $provider = new ActiveDataProvider([ + 'query' => Order::find()->orderBy('id'), + ]); + $orders = $provider->getModels(); + $this->assertEquals(3, count($orders)); + $this->assertTrue($orders[0] instanceof Order); + $this->assertTrue($orders[1] instanceof Order); + $this->assertTrue($orders[2] instanceof Order); + $this->assertEquals([1, 2, 3], $provider->getKeys()); + + $provider = new ActiveDataProvider([ + 'query' => Order::find(), + 'pagination' => [ + 'pageSize' => 2, + ] + ]); + $orders = $provider->getModels(); + $this->assertEquals(2, count($orders)); + } + + public function testActiveRelation() + { + /** @var Customer $customer */ + $customer = Customer::find(2); + $provider = new ActiveDataProvider([ + 'query' => $customer->getOrders(), + ]); + $orders = $provider->getModels(); + $this->assertEquals(2, count($orders)); + $this->assertTrue($orders[0] instanceof Order); + $this->assertTrue($orders[1] instanceof Order); + $this->assertEquals([2, 3], $provider->getKeys()); + + $provider = new ActiveDataProvider([ + 'query' => $customer->getOrders(), + 'pagination' => [ + 'pageSize' => 1, + ] + ]); + $orders = $provider->getModels(); + $this->assertEquals(1, count($orders)); + } + + public function testActiveRelationVia() + { + /** @var Order $order */ + $order = Order::find(2); + $provider = new ActiveDataProvider([ + 'query' => $order->getItems(), + ]); + $items = $provider->getModels(); + $this->assertEquals(3, count($items)); + $this->assertTrue($items[0] instanceof Item); + $this->assertTrue($items[1] instanceof Item); + $this->assertTrue($items[2] instanceof Item); + $this->assertEquals([3, 4, 5], $provider->getKeys()); + + $provider = new ActiveDataProvider([ + 'query' => $order->getItems(), + 'pagination' => [ + 'pageSize' => 2, + ] + ]); + $items = $provider->getModels(); + $this->assertEquals(2, count($items)); + } + + public function testActiveRelationViaTable() + { + /** @var Order $order */ + $order = Order::find(1); + $provider = new ActiveDataProvider([ + 'query' => $order->getBooks(), + ]); + $items = $provider->getModels(); + $this->assertEquals(2, count($items)); + $this->assertTrue($items[0] instanceof Item); + $this->assertTrue($items[1] instanceof Item); + + $provider = new ActiveDataProvider([ + 'query' => $order->getBooks(), + 'pagination' => [ + 'pageSize' => 1, + ] + ]); + $items = $provider->getModels(); + $this->assertEquals(1, count($items)); + } + + public function testQuery() + { + $query = new Query; + $provider = new ActiveDataProvider([ + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order')->orderBy('id'), + ]); + $orders = $provider->getModels(); + $this->assertEquals(3, count($orders)); + $this->assertTrue(is_array($orders[0])); + $this->assertEquals([0, 1, 2], $provider->getKeys()); + + $query = new Query; + $provider = new ActiveDataProvider([ + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order'), + 'pagination' => [ + 'pageSize' => 2, + ] + ]); + $orders = $provider->getModels(); + $this->assertEquals(2, count($orders)); + } + + public function testRefresh() + { + $query = new Query; + $provider = new ActiveDataProvider([ + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order')->orderBy('id'), + ]); + $this->assertEquals(3, count($provider->getModels())); + + $provider->getPagination()->pageSize = 2; + $this->assertEquals(3, count($provider->getModels())); + $provider->refresh(); + $this->assertEquals(2, count($provider->getModels())); + } + + public function testPaginationBeforeModels() + { + $query = new Query; + $provider = new ActiveDataProvider([ + 'db' => $this->getConnection(), + 'query' => $query->from('tbl_order')->orderBy('id'), + ]); + $pagination = $provider->getPagination(); + $this->assertEquals(0, $pagination->getPageCount()); + $this->assertCount(3, $provider->getModels()); + $this->assertEquals(1, $pagination->getPageCount()); + + $provider->getPagination()->pageSize = 2; + $this->assertEquals(3, count($provider->getModels())); + $provider->refresh(); + $this->assertEquals(2, count($provider->getModels())); + } } diff --git a/tests/unit/framework/data/SortTest.php b/tests/unit/framework/data/SortTest.php index b0d16def49b..661ede5ab63 100644 --- a/tests/unit/framework/data/SortTest.php +++ b/tests/unit/framework/data/SortTest.php @@ -19,160 +19,160 @@ */ class SortTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - public function testGetOrders() - { - $sort = new Sort([ - 'attributes' => [ - 'age', - 'name' => [ - 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - ], - ], - 'params' => [ - 'sort' => 'age,-name' - ], - 'enableMultiSort' => true, - ]); - - $orders = $sort->getOrders(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(SORT_ASC, $orders['age']); - $this->assertEquals(SORT_DESC, $orders['first_name']); - $this->assertEquals(SORT_DESC, $orders['last_name']); - - $sort->enableMultiSort = false; - $orders = $sort->getOrders(true); - $this->assertEquals(1, count($orders)); - $this->assertEquals(SORT_ASC, $orders['age']); - } - - public function testGetAttributeOrders() - { - $sort = new Sort([ - 'attributes' => [ - 'age', - 'name' => [ - 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - ], - ], - 'params' => [ - 'sort' => 'age,-name' - ], - 'enableMultiSort' => true, - ]); - - $orders = $sort->getAttributeOrders(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(SORT_ASC, $orders['age']); - $this->assertEquals(SORT_DESC, $orders['name']); - - $sort->enableMultiSort = false; - $orders = $sort->getAttributeOrders(true); - $this->assertEquals(1, count($orders)); - $this->assertEquals(SORT_ASC, $orders['age']); - } - - public function testGetAttributeOrder() - { - $sort = new Sort([ - 'attributes' => [ - 'age', - 'name' => [ - 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - ], - ], - 'params' => [ - 'sort' => 'age,-name' - ], - 'enableMultiSort' => true, - ]); - - $this->assertEquals(SORT_ASC, $sort->getAttributeOrder('age')); - $this->assertEquals(SORT_DESC, $sort->getAttributeOrder('name')); - $this->assertNull($sort->getAttributeOrder('xyz')); - } - - public function testCreateSortParam() - { - $sort = new Sort([ - 'attributes' => [ - 'age', - 'name' => [ - 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - ], - ], - 'params' => [ - 'sort' => 'age,-name' - ], - 'enableMultiSort' => true, - 'route' => 'site/index', - ]); - - $this->assertEquals('-age,-name', $sort->createSortParam('age')); - $this->assertEquals('name,age', $sort->createSortParam('name')); - } - - public function testCreateUrl() - { - $manager = new UrlManager([ - 'baseUrl' => '/index.php', - 'cache' => null, - ]); - - $sort = new Sort([ - 'attributes' => [ - 'age', - 'name' => [ - 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - ], - ], - 'params' => [ - 'sort' => 'age,-name' - ], - 'enableMultiSort' => true, - 'urlManager' => $manager, - 'route' => 'site/index', - ]); - - $this->assertEquals('/index.php?r=site/index&sort=-age%2C-name', $sort->createUrl('age')); - $this->assertEquals('/index.php?r=site/index&sort=name%2Cage', $sort->createUrl('name')); - } - - public function testLink() - { - $this->mockApplication(); - $manager = new UrlManager([ - 'baseUrl' => '/index.php', - 'cache' => null, - ]); - - $sort = new Sort([ - 'attributes' => [ - 'age', - 'name' => [ - 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], - 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], - ], - ], - 'params' => [ - 'sort' => 'age,-name' - ], - 'enableMultiSort' => true, - 'urlManager' => $manager, - 'route' => 'site/index', - ]); - - $this->assertEquals('Age', $sort->link('age')); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testGetOrders() + { + $sort = new Sort([ + 'attributes' => [ + 'age', + 'name' => [ + 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + ], + ], + 'params' => [ + 'sort' => 'age,-name' + ], + 'enableMultiSort' => true, + ]); + + $orders = $sort->getOrders(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(SORT_ASC, $orders['age']); + $this->assertEquals(SORT_DESC, $orders['first_name']); + $this->assertEquals(SORT_DESC, $orders['last_name']); + + $sort->enableMultiSort = false; + $orders = $sort->getOrders(true); + $this->assertEquals(1, count($orders)); + $this->assertEquals(SORT_ASC, $orders['age']); + } + + public function testGetAttributeOrders() + { + $sort = new Sort([ + 'attributes' => [ + 'age', + 'name' => [ + 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + ], + ], + 'params' => [ + 'sort' => 'age,-name' + ], + 'enableMultiSort' => true, + ]); + + $orders = $sort->getAttributeOrders(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(SORT_ASC, $orders['age']); + $this->assertEquals(SORT_DESC, $orders['name']); + + $sort->enableMultiSort = false; + $orders = $sort->getAttributeOrders(true); + $this->assertEquals(1, count($orders)); + $this->assertEquals(SORT_ASC, $orders['age']); + } + + public function testGetAttributeOrder() + { + $sort = new Sort([ + 'attributes' => [ + 'age', + 'name' => [ + 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + ], + ], + 'params' => [ + 'sort' => 'age,-name' + ], + 'enableMultiSort' => true, + ]); + + $this->assertEquals(SORT_ASC, $sort->getAttributeOrder('age')); + $this->assertEquals(SORT_DESC, $sort->getAttributeOrder('name')); + $this->assertNull($sort->getAttributeOrder('xyz')); + } + + public function testCreateSortParam() + { + $sort = new Sort([ + 'attributes' => [ + 'age', + 'name' => [ + 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + ], + ], + 'params' => [ + 'sort' => 'age,-name' + ], + 'enableMultiSort' => true, + 'route' => 'site/index', + ]); + + $this->assertEquals('-age,-name', $sort->createSortParam('age')); + $this->assertEquals('name,age', $sort->createSortParam('name')); + } + + public function testCreateUrl() + { + $manager = new UrlManager([ + 'baseUrl' => '/index.php', + 'cache' => null, + ]); + + $sort = new Sort([ + 'attributes' => [ + 'age', + 'name' => [ + 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + ], + ], + 'params' => [ + 'sort' => 'age,-name' + ], + 'enableMultiSort' => true, + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $this->assertEquals('/index.php?r=site/index&sort=-age%2C-name', $sort->createUrl('age')); + $this->assertEquals('/index.php?r=site/index&sort=name%2Cage', $sort->createUrl('name')); + } + + public function testLink() + { + $this->mockApplication(); + $manager = new UrlManager([ + 'baseUrl' => '/index.php', + 'cache' => null, + ]); + + $sort = new Sort([ + 'attributes' => [ + 'age', + 'name' => [ + 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], + 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], + ], + ], + 'params' => [ + 'sort' => 'age,-name' + ], + 'enableMultiSort' => true, + 'urlManager' => $manager, + 'route' => 'site/index', + ]); + + $this->assertEquals('Age', $sort->link('age')); + } } diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 646ad94cac3..1f53e46b013 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -16,474 +16,474 @@ */ class ActiveRecordTest extends DatabaseTestCase { - use ActiveRecordTestTrait; - - protected function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - } - - public function callCustomerFind($q = null) - { - return Customer::find($q); - } - - public function callOrderFind($q = null) - { - return Order::find($q); - } - - public function callOrderItemFind($q = null) - { - return OrderItem::find($q); - } - - public function callItemFind($q = null) - { - return Item::find($q); - } - - public function getCustomerClass() - { - return Customer::className(); - } - - public function getItemClass() - { - return Item::className(); - } - - public function getOrderClass() - { - return Order::className(); - } - - public function getOrderItemClass() - { - return OrderItem::className(); - } - - public function testCustomColumns() - { - // find custom column - $customer = $this->callCustomerFind()->select(['*', '(status*2) AS status2']) - ->where(['name' => 'user3'])->one(); - $this->assertEquals(3, $customer->id); - $this->assertEquals(4, $customer->status2); - } - - public function testStatisticalFind() - { - // find count, sum, average, min, max, scalar - $this->assertEquals(3, $this->callCustomerFind()->count()); - $this->assertEquals(2, $this->callCustomerFind()->where('id=1 OR id=2')->count()); - $this->assertEquals(6, $this->callCustomerFind()->sum('id')); - $this->assertEquals(2, $this->callCustomerFind()->average('id')); - $this->assertEquals(1, $this->callCustomerFind()->min('id')); - $this->assertEquals(3, $this->callCustomerFind()->max('id')); - $this->assertEquals(3, $this->callCustomerFind()->select('COUNT(*)')->scalar()); - } - - public function testFindScalar() - { - // query scalar - $customerName = $this->callCustomerFind()->where(['id' => 2])->select('name')->scalar(); - $this->assertEquals('user2', $customerName); - } - - public function testFindColumn() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->select('name')->column()); - $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->select('name')->column()); - } - - public function testFindBySql() - { - // find one - $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user3', $customer->name); - - // find all - $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); - $this->assertEquals(3, count($customers)); - - // find with parameter binding - $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', [':id' => 2])->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - } - - public function testFindLazyViaTable() - { - /** @var Order $order */ - $order = Order::find(1); - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->books)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - - $order = Order::find(2); - $this->assertEquals(2, $order->id); - $this->assertEquals(0, count($order->books)); - } - - public function testFindEagerViaTable() - { - $orders = Order::find()->with('books')->orderBy('id')->all(); - $this->assertEquals(3, count($orders)); - - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->books)); - $this->assertEquals(1, $order->books[0]->id); - $this->assertEquals(2, $order->books[1]->id); - - $order = $orders[1]; - $this->assertEquals(2, $order->id); - $this->assertEquals(0, count($order->books)); - - $order = $orders[2]; - $this->assertEquals(3, $order->id); - $this->assertEquals(1, count($order->books)); - $this->assertEquals(2, $order->books[0]->id); - - // https://github.com/yiisoft/yii2/issues/1402 - $orders = Order::find()->with('books')->orderBy('id')->asArray()->all(); - $this->assertEquals(3, count($orders)); - - $order = $orders[0]; - $this->assertTrue(is_array($order)); - $this->assertEquals(1, $order['id']); - $this->assertEquals(2, count($order['books'])); - $this->assertEquals(1, $order['books'][0]['id']); - $this->assertEquals(2, $order['books'][1]['id']); - } - - // deeply nested table relation - public function testDeeplyNestedTableRelation() - { - /** @var Customer $customer */ - $customer = $this->callCustomerFind(1); - $this->assertNotNull($customer); - - $items = $customer->orderItems; - - $this->assertEquals(2, count($items)); - $this->assertInstanceOf(Item::className(), $items[0]); - $this->assertInstanceOf(Item::className(), $items[1]); - $this->assertEquals(1, $items[0]->id); - $this->assertEquals(2, $items[1]->id); - } - - public function testStoreNull() - { - $record = new NullValues(); - $this->assertNull($record->var1); - $this->assertNull($record->var2); - $this->assertNull($record->var3); - $this->assertNull($record->stringcol); - - $record->id = 1; - - $record->var1 = 123; - $record->var2 = 456; - $record->var3 = 789; - $record->stringcol = 'hello!'; - - $record->save(false); - $this->assertTrue($record->refresh()); - - $this->assertEquals(123, $record->var1); - $this->assertEquals(456, $record->var2); - $this->assertEquals(789, $record->var3); - $this->assertEquals('hello!', $record->stringcol); - - $record->var1 = null; - $record->var2 = null; - $record->var3 = null; - $record->stringcol = null; - - $record->save(false); - $this->assertTrue($record->refresh()); - - $this->assertNull($record->var1); - $this->assertNull($record->var2); - $this->assertNull($record->var3); - $this->assertNull($record->stringcol); - - $record->var1 = 0; - $record->var2 = 0; - $record->var3 = 0; - $record->stringcol = ''; - - $record->save(false); - $this->assertTrue($record->refresh()); - - $this->assertEquals(0, $record->var1); - $this->assertEquals(0, $record->var2); - $this->assertEquals(0, $record->var3); - $this->assertEquals('', $record->stringcol); - } - - public function testStoreEmpty() - { - $record = new NullValues(); - $record->id = 1; - - // this is to simulate empty html form submission - $record->var1 = ''; - $record->var2 = ''; - $record->var3 = ''; - $record->stringcol = ''; - - $record->save(false); - $this->assertTrue($record->refresh()); - - // https://github.com/yiisoft/yii2/commit/34945b0b69011bc7cab684c7f7095d837892a0d4#commitcomment-4458225 - $this->assertTrue($record->var1 === $record->var2); - $this->assertTrue($record->var2 === $record->var3); - } - - public function testIsPrimaryKey() - { - $this->assertFalse(Customer::isPrimaryKey([])); - $this->assertTrue(Customer::isPrimaryKey(['id'])); - $this->assertFalse(Customer::isPrimaryKey(['id', 'name'])); - $this->assertFalse(Customer::isPrimaryKey(['name'])); - $this->assertFalse(Customer::isPrimaryKey(['name', 'email'])); - - $this->assertFalse(OrderItem::isPrimaryKey([])); - $this->assertFalse(OrderItem::isPrimaryKey(['order_id'])); - $this->assertFalse(OrderItem::isPrimaryKey(['item_id'])); - $this->assertFalse(OrderItem::isPrimaryKey(['quantity'])); - $this->assertFalse(OrderItem::isPrimaryKey(['quantity', 'subtotal'])); - $this->assertTrue(OrderItem::isPrimaryKey(['order_id', 'item_id'])); - $this->assertFalse(OrderItem::isPrimaryKey(['order_id', 'item_id', 'quantity'])); - } - - public function testJoinWith() - { - // left join and eager loading - $orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id DESC, tbl_order.id')->all(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - $this->assertEquals(1, $orders[2]->id); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - $this->assertTrue($orders[1]->isRelationPopulated('customer')); - $this->assertTrue($orders[2]->isRelationPopulated('customer')); - - // inner join filtering and eager loading - $orders = Order::find()->innerJoinWith([ - 'customer' => function ($query) { - $query->where('tbl_customer.id=2'); - }, - ])->orderBy('tbl_order.id')->all(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - $this->assertTrue($orders[1]->isRelationPopulated('customer')); - - // inner join filtering, eager loading, conditions on both primary and relation - $orders = Order::find()->innerJoinWith([ - 'customer' => function ($query) { - $query->where(['tbl_customer.id' => 2]); - }, - ])->where(['tbl_order.id' => [1, 2]])->orderBy('tbl_order.id')->all(); - $this->assertEquals(1, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - - // inner join filtering without eager loading - $orders = Order::find()->innerJoinWith([ - 'customer' => function ($query) { - $query->where('tbl_customer.id=2'); - }, - ], false)->orderBy('tbl_order.id')->all(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - $this->assertFalse($orders[0]->isRelationPopulated('customer')); - $this->assertFalse($orders[1]->isRelationPopulated('customer')); - - // inner join filtering without eager loading, conditions on both primary and relation - $orders = Order::find()->innerJoinWith([ - 'customer' => function ($query) { - $query->where(['tbl_customer.id' => 2]); - }, - ], false)->where(['tbl_order.id' => [1, 2]])->orderBy('tbl_order.id')->all(); - $this->assertEquals(1, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertFalse($orders[0]->isRelationPopulated('customer')); - - // join with via-relation - $orders = Order::find()->innerJoinWith('books')->orderBy('tbl_order.id')->all(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(1, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - $this->assertTrue($orders[0]->isRelationPopulated('books')); - $this->assertTrue($orders[1]->isRelationPopulated('books')); - $this->assertEquals(2, count($orders[0]->books)); - $this->assertEquals(1, count($orders[1]->books)); - - // join with sub-relation - $orders = Order::find()->innerJoinWith([ - 'items' => function ($q) { - $q->orderBy('tbl_item.id'); - }, - 'items.category' => function ($q) { - $q->where('tbl_category.id = 2'); - }, - ])->orderBy('tbl_order.id')->all(); - $this->assertEquals(1, count($orders)); - $this->assertTrue($orders[0]->isRelationPopulated('items')); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, count($orders[0]->items)); - $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); - $this->assertEquals(2, $orders[0]->items[0]->category->id); - - // join with table alias - $orders = Order::find()->joinWith([ - 'customer' => function ($q) { - $q->from('tbl_customer c'); - } - ])->orderBy('c.id DESC, tbl_order.id')->all(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - $this->assertEquals(1, $orders[2]->id); - $this->assertTrue($orders[0]->isRelationPopulated('customer')); - $this->assertTrue($orders[1]->isRelationPopulated('customer')); - $this->assertTrue($orders[2]->isRelationPopulated('customer')); - - // join with ON condition - $orders = Order::find()->joinWith('books2')->orderBy('tbl_order.id')->all(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(1, $orders[0]->id); - $this->assertEquals(2, $orders[1]->id); - $this->assertEquals(3, $orders[2]->id); - $this->assertTrue($orders[0]->isRelationPopulated('books2')); - $this->assertTrue($orders[1]->isRelationPopulated('books2')); - $this->assertTrue($orders[2]->isRelationPopulated('books2')); - $this->assertEquals(2, count($orders[0]->books2)); - $this->assertEquals(0, count($orders[1]->books2)); - $this->assertEquals(1, count($orders[2]->books2)); - - // lazy loading with ON condition - $order = Order::find(1); - $this->assertEquals(2, count($order->books2)); - $order = Order::find(2); - $this->assertEquals(0, count($order->books2)); - $order = Order::find(3); - $this->assertEquals(1, count($order->books2)); - - // eager loading with ON condition - $orders = Order::find()->with('books2')->all(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(1, $orders[0]->id); - $this->assertEquals(2, $orders[1]->id); - $this->assertEquals(3, $orders[2]->id); - $this->assertTrue($orders[0]->isRelationPopulated('books2')); - $this->assertTrue($orders[1]->isRelationPopulated('books2')); - $this->assertTrue($orders[2]->isRelationPopulated('books2')); - $this->assertEquals(2, count($orders[0]->books2)); - $this->assertEquals(0, count($orders[1]->books2)); - $this->assertEquals(1, count($orders[2]->books2)); - - // join with count and query - $query = Order::find()->joinWith('customer'); - $count = $query->count(); - $this->assertEquals(3, $count); - $orders = $query->all(); - $this->assertEquals(3, count($orders)); - } - - public function testJoinWithAndScope() - { - // hasOne inner join - $customers = Customer::find()->active()->innerJoinWith('profile')->orderBy('tbl_customer.id')->all(); - $this->assertEquals(1, count($customers)); - $this->assertEquals(1, $customers[0]->id); - $this->assertTrue($customers[0]->isRelationPopulated('profile')); - - // hasOne outer join - $customers = Customer::find()->active()->joinWith('profile')->orderBy('tbl_customer.id')->all(); - $this->assertEquals(2, count($customers)); - $this->assertEquals(1, $customers[0]->id); - $this->assertEquals(2, $customers[1]->id); - $this->assertTrue($customers[0]->isRelationPopulated('profile')); - $this->assertTrue($customers[1]->isRelationPopulated('profile')); - $this->assertInstanceOf(Profile::className(), $customers[0]->profile); - $this->assertNull($customers[1]->profile); - - // hasMany - $customers = Customer::find()->active()->joinWith([ - 'orders' => function ($q) { - $q->orderBy('tbl_order.id'); - } - ])->orderBy('tbl_customer.id DESC, tbl_order.id')->all(); - $this->assertEquals(2, count($customers)); - $this->assertEquals(2, $customers[0]->id); - $this->assertEquals(1, $customers[1]->id); - $this->assertTrue($customers[0]->isRelationPopulated('orders')); - $this->assertTrue($customers[1]->isRelationPopulated('orders')); - - } - - public function testInverseOf() - { - // eager loading: find one and all - $customer = Customer::find()->with('orders2')->where(['id' => 1])->one(); - $this->assertTrue($customer->orders2[0]->customer2 === $customer); - $customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->all(); - $this->assertTrue($customers[0]->orders2[0]->customer2 === $customers[0]); - $this->assertTrue(empty($customers[1]->orders2)); - // lazy loading - $customer = Customer::find(2); - $orders = $customer->orders2; - $this->assertTrue(count($orders) === 2); - $this->assertTrue($customer->orders2[0]->customer2 === $customer); - $this->assertTrue($customer->orders2[1]->customer2 === $customer); - // ad-hoc lazy loading - $customer = Customer::find(2); - $orders = $customer->getOrders2()->all(); - $this->assertTrue(count($orders) === 2); - $this->assertTrue($customer->orders2[0]->customer2 === $customer); - $this->assertTrue($customer->orders2[1]->customer2 === $customer); - - // the other way around - $customer = Customer::find()->with('orders2')->where(['id' => 1])->asArray()->one(); - $this->assertTrue($customer['orders2'][0]['customer2']['id'] === $customer['id']); - $customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->asArray()->all(); - $this->assertTrue($customer['orders2'][0]['customer2']['id'] === $customers[0]['id']); - $this->assertTrue(empty($customers[1]['orders2'])); - - $orders = Order::find()->with('customer2')->where(['id' => 1])->all(); - $this->assertTrue($orders[0]->customer2->orders2 === [$orders[0]]); - $order = Order::find()->with('customer2')->where(['id' => 1])->one(); - $this->assertTrue($order->customer2->orders2 === [$order]); - - $orders = Order::find()->with('customer2')->where(['id' => 1])->asArray()->all(); - $this->assertTrue($orders[0]['customer2']['orders2'][0]['id'] === $orders[0]['id']); - $order = Order::find()->with('customer2')->where(['id' => 1])->asArray()->one(); - $this->assertTrue($order['customer2']['orders2'][0]['id'] === $orders[0]['id']); - - $orders = Order::find()->with('customer2')->where(['id' => [1, 3]])->all(); - $this->assertTrue($orders[0]->customer2->orders2 === [$orders[0]]); - $this->assertTrue($orders[1]->customer2->orders2 === [$orders[1]]); - - $orders = Order::find()->with('customer2')->where(['id' => [2, 3]])->orderBy('id')->all(); - $this->assertTrue($orders[0]->customer2->orders2 === $orders); - $this->assertTrue($orders[1]->customer2->orders2 === $orders); - - $orders = Order::find()->with('customer2')->where(['id' => [2, 3]])->orderBy('id')->asArray()->all(); - $this->assertTrue($orders[0]['customer2']['orders2'][0]['id'] === $orders[0]['id']); - $this->assertTrue($orders[0]['customer2']['orders2'][1]['id'] === $orders[1]['id']); - $this->assertTrue($orders[1]['customer2']['orders2'][0]['id'] === $orders[0]['id']); - $this->assertTrue($orders[1]['customer2']['orders2'][1]['id'] === $orders[1]['id']); - } + use ActiveRecordTestTrait; + + protected function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } + + public function callCustomerFind($q = null) + { + return Customer::find($q); + } + + public function callOrderFind($q = null) + { + return Order::find($q); + } + + public function callOrderItemFind($q = null) + { + return OrderItem::find($q); + } + + public function callItemFind($q = null) + { + return Item::find($q); + } + + public function getCustomerClass() + { + return Customer::className(); + } + + public function getItemClass() + { + return Item::className(); + } + + public function getOrderClass() + { + return Order::className(); + } + + public function getOrderItemClass() + { + return OrderItem::className(); + } + + public function testCustomColumns() + { + // find custom column + $customer = $this->callCustomerFind()->select(['*', '(status*2) AS status2']) + ->where(['name' => 'user3'])->one(); + $this->assertEquals(3, $customer->id); + $this->assertEquals(4, $customer->status2); + } + + public function testStatisticalFind() + { + // find count, sum, average, min, max, scalar + $this->assertEquals(3, $this->callCustomerFind()->count()); + $this->assertEquals(2, $this->callCustomerFind()->where('id=1 OR id=2')->count()); + $this->assertEquals(6, $this->callCustomerFind()->sum('id')); + $this->assertEquals(2, $this->callCustomerFind()->average('id')); + $this->assertEquals(1, $this->callCustomerFind()->min('id')); + $this->assertEquals(3, $this->callCustomerFind()->max('id')); + $this->assertEquals(3, $this->callCustomerFind()->select('COUNT(*)')->scalar()); + } + + public function testFindScalar() + { + // query scalar + $customerName = $this->callCustomerFind()->where(['id' => 2])->select('name')->scalar(); + $this->assertEquals('user2', $customerName); + } + + public function testFindColumn() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->select('name')->column()); + $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->select('name')->column()); + } + + public function testFindBySql() + { + // find one + $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user3', $customer->name); + + // find all + $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); + $this->assertEquals(3, count($customers)); + + // find with parameter binding + $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', [':id' => 2])->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + } + + public function testFindLazyViaTable() + { + /** @var Order $order */ + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(2); + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + } + + public function testFindEagerViaTable() + { + $orders = Order::find()->with('books')->orderBy('id')->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->books[0]->id); + $this->assertEquals(2, $order->books[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertEquals(1, count($order->books)); + $this->assertEquals(2, $order->books[0]->id); + + // https://github.com/yiisoft/yii2/issues/1402 + $orders = Order::find()->with('books')->orderBy('id')->asArray()->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertTrue(is_array($order)); + $this->assertEquals(1, $order['id']); + $this->assertEquals(2, count($order['books'])); + $this->assertEquals(1, $order['books'][0]['id']); + $this->assertEquals(2, $order['books'][1]['id']); + } + + // deeply nested table relation + public function testDeeplyNestedTableRelation() + { + /** @var Customer $customer */ + $customer = $this->callCustomerFind(1); + $this->assertNotNull($customer); + + $items = $customer->orderItems; + + $this->assertEquals(2, count($items)); + $this->assertInstanceOf(Item::className(), $items[0]); + $this->assertInstanceOf(Item::className(), $items[1]); + $this->assertEquals(1, $items[0]->id); + $this->assertEquals(2, $items[1]->id); + } + + public function testStoreNull() + { + $record = new NullValues(); + $this->assertNull($record->var1); + $this->assertNull($record->var2); + $this->assertNull($record->var3); + $this->assertNull($record->stringcol); + + $record->id = 1; + + $record->var1 = 123; + $record->var2 = 456; + $record->var3 = 789; + $record->stringcol = 'hello!'; + + $record->save(false); + $this->assertTrue($record->refresh()); + + $this->assertEquals(123, $record->var1); + $this->assertEquals(456, $record->var2); + $this->assertEquals(789, $record->var3); + $this->assertEquals('hello!', $record->stringcol); + + $record->var1 = null; + $record->var2 = null; + $record->var3 = null; + $record->stringcol = null; + + $record->save(false); + $this->assertTrue($record->refresh()); + + $this->assertNull($record->var1); + $this->assertNull($record->var2); + $this->assertNull($record->var3); + $this->assertNull($record->stringcol); + + $record->var1 = 0; + $record->var2 = 0; + $record->var3 = 0; + $record->stringcol = ''; + + $record->save(false); + $this->assertTrue($record->refresh()); + + $this->assertEquals(0, $record->var1); + $this->assertEquals(0, $record->var2); + $this->assertEquals(0, $record->var3); + $this->assertEquals('', $record->stringcol); + } + + public function testStoreEmpty() + { + $record = new NullValues(); + $record->id = 1; + + // this is to simulate empty html form submission + $record->var1 = ''; + $record->var2 = ''; + $record->var3 = ''; + $record->stringcol = ''; + + $record->save(false); + $this->assertTrue($record->refresh()); + + // https://github.com/yiisoft/yii2/commit/34945b0b69011bc7cab684c7f7095d837892a0d4#commitcomment-4458225 + $this->assertTrue($record->var1 === $record->var2); + $this->assertTrue($record->var2 === $record->var3); + } + + public function testIsPrimaryKey() + { + $this->assertFalse(Customer::isPrimaryKey([])); + $this->assertTrue(Customer::isPrimaryKey(['id'])); + $this->assertFalse(Customer::isPrimaryKey(['id', 'name'])); + $this->assertFalse(Customer::isPrimaryKey(['name'])); + $this->assertFalse(Customer::isPrimaryKey(['name', 'email'])); + + $this->assertFalse(OrderItem::isPrimaryKey([])); + $this->assertFalse(OrderItem::isPrimaryKey(['order_id'])); + $this->assertFalse(OrderItem::isPrimaryKey(['item_id'])); + $this->assertFalse(OrderItem::isPrimaryKey(['quantity'])); + $this->assertFalse(OrderItem::isPrimaryKey(['quantity', 'subtotal'])); + $this->assertTrue(OrderItem::isPrimaryKey(['order_id', 'item_id'])); + $this->assertFalse(OrderItem::isPrimaryKey(['order_id', 'item_id', 'quantity'])); + } + + public function testJoinWith() + { + // left join and eager loading + $orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id DESC, tbl_order.id')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + + // inner join filtering and eager loading + $orders = Order::find()->innerJoinWith([ + 'customer' => function ($query) { + $query->where('tbl_customer.id=2'); + }, + ])->orderBy('tbl_order.id')->all(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + + // inner join filtering, eager loading, conditions on both primary and relation + $orders = Order::find()->innerJoinWith([ + 'customer' => function ($query) { + $query->where(['tbl_customer.id' => 2]); + }, + ])->where(['tbl_order.id' => [1, 2]])->orderBy('tbl_order.id')->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + + // inner join filtering without eager loading + $orders = Order::find()->innerJoinWith([ + 'customer' => function ($query) { + $query->where('tbl_customer.id=2'); + }, + ], false)->orderBy('tbl_order.id')->all(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertFalse($orders[0]->isRelationPopulated('customer')); + $this->assertFalse($orders[1]->isRelationPopulated('customer')); + + // inner join filtering without eager loading, conditions on both primary and relation + $orders = Order::find()->innerJoinWith([ + 'customer' => function ($query) { + $query->where(['tbl_customer.id' => 2]); + }, + ], false)->where(['tbl_order.id' => [1, 2]])->orderBy('tbl_order.id')->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertFalse($orders[0]->isRelationPopulated('customer')); + + // join with via-relation + $orders = Order::find()->innerJoinWith('books')->orderBy('tbl_order.id')->all(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books')); + $this->assertTrue($orders[1]->isRelationPopulated('books')); + $this->assertEquals(2, count($orders[0]->books)); + $this->assertEquals(1, count($orders[1]->books)); + + // join with sub-relation + $orders = Order::find()->innerJoinWith([ + 'items' => function ($q) { + $q->orderBy('tbl_item.id'); + }, + 'items.category' => function ($q) { + $q->where('tbl_category.id = 2'); + }, + ])->orderBy('tbl_order.id')->all(); + $this->assertEquals(1, count($orders)); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, count($orders[0]->items)); + $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); + $this->assertEquals(2, $orders[0]->items[0]->category->id); + + // join with table alias + $orders = Order::find()->joinWith([ + 'customer' => function ($q) { + $q->from('tbl_customer c'); + } + ])->orderBy('c.id DESC, tbl_order.id')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + + // join with ON condition + $orders = Order::find()->joinWith('books2')->orderBy('tbl_order.id')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(2, $orders[1]->id); + $this->assertEquals(3, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books2')); + $this->assertTrue($orders[1]->isRelationPopulated('books2')); + $this->assertTrue($orders[2]->isRelationPopulated('books2')); + $this->assertEquals(2, count($orders[0]->books2)); + $this->assertEquals(0, count($orders[1]->books2)); + $this->assertEquals(1, count($orders[2]->books2)); + + // lazy loading with ON condition + $order = Order::find(1); + $this->assertEquals(2, count($order->books2)); + $order = Order::find(2); + $this->assertEquals(0, count($order->books2)); + $order = Order::find(3); + $this->assertEquals(1, count($order->books2)); + + // eager loading with ON condition + $orders = Order::find()->with('books2')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(2, $orders[1]->id); + $this->assertEquals(3, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books2')); + $this->assertTrue($orders[1]->isRelationPopulated('books2')); + $this->assertTrue($orders[2]->isRelationPopulated('books2')); + $this->assertEquals(2, count($orders[0]->books2)); + $this->assertEquals(0, count($orders[1]->books2)); + $this->assertEquals(1, count($orders[2]->books2)); + + // join with count and query + $query = Order::find()->joinWith('customer'); + $count = $query->count(); + $this->assertEquals(3, $count); + $orders = $query->all(); + $this->assertEquals(3, count($orders)); + } + + public function testJoinWithAndScope() + { + // hasOne inner join + $customers = Customer::find()->active()->innerJoinWith('profile')->orderBy('tbl_customer.id')->all(); + $this->assertEquals(1, count($customers)); + $this->assertEquals(1, $customers[0]->id); + $this->assertTrue($customers[0]->isRelationPopulated('profile')); + + // hasOne outer join + $customers = Customer::find()->active()->joinWith('profile')->orderBy('tbl_customer.id')->all(); + $this->assertEquals(2, count($customers)); + $this->assertEquals(1, $customers[0]->id); + $this->assertEquals(2, $customers[1]->id); + $this->assertTrue($customers[0]->isRelationPopulated('profile')); + $this->assertTrue($customers[1]->isRelationPopulated('profile')); + $this->assertInstanceOf(Profile::className(), $customers[0]->profile); + $this->assertNull($customers[1]->profile); + + // hasMany + $customers = Customer::find()->active()->joinWith([ + 'orders' => function ($q) { + $q->orderBy('tbl_order.id'); + } + ])->orderBy('tbl_customer.id DESC, tbl_order.id')->all(); + $this->assertEquals(2, count($customers)); + $this->assertEquals(2, $customers[0]->id); + $this->assertEquals(1, $customers[1]->id); + $this->assertTrue($customers[0]->isRelationPopulated('orders')); + $this->assertTrue($customers[1]->isRelationPopulated('orders')); + + } + + public function testInverseOf() + { + // eager loading: find one and all + $customer = Customer::find()->with('orders2')->where(['id' => 1])->one(); + $this->assertTrue($customer->orders2[0]->customer2 === $customer); + $customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->all(); + $this->assertTrue($customers[0]->orders2[0]->customer2 === $customers[0]); + $this->assertTrue(empty($customers[1]->orders2)); + // lazy loading + $customer = Customer::find(2); + $orders = $customer->orders2; + $this->assertTrue(count($orders) === 2); + $this->assertTrue($customer->orders2[0]->customer2 === $customer); + $this->assertTrue($customer->orders2[1]->customer2 === $customer); + // ad-hoc lazy loading + $customer = Customer::find(2); + $orders = $customer->getOrders2()->all(); + $this->assertTrue(count($orders) === 2); + $this->assertTrue($customer->orders2[0]->customer2 === $customer); + $this->assertTrue($customer->orders2[1]->customer2 === $customer); + + // the other way around + $customer = Customer::find()->with('orders2')->where(['id' => 1])->asArray()->one(); + $this->assertTrue($customer['orders2'][0]['customer2']['id'] === $customer['id']); + $customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->asArray()->all(); + $this->assertTrue($customer['orders2'][0]['customer2']['id'] === $customers[0]['id']); + $this->assertTrue(empty($customers[1]['orders2'])); + + $orders = Order::find()->with('customer2')->where(['id' => 1])->all(); + $this->assertTrue($orders[0]->customer2->orders2 === [$orders[0]]); + $order = Order::find()->with('customer2')->where(['id' => 1])->one(); + $this->assertTrue($order->customer2->orders2 === [$order]); + + $orders = Order::find()->with('customer2')->where(['id' => 1])->asArray()->all(); + $this->assertTrue($orders[0]['customer2']['orders2'][0]['id'] === $orders[0]['id']); + $order = Order::find()->with('customer2')->where(['id' => 1])->asArray()->one(); + $this->assertTrue($order['customer2']['orders2'][0]['id'] === $orders[0]['id']); + + $orders = Order::find()->with('customer2')->where(['id' => [1, 3]])->all(); + $this->assertTrue($orders[0]->customer2->orders2 === [$orders[0]]); + $this->assertTrue($orders[1]->customer2->orders2 === [$orders[1]]); + + $orders = Order::find()->with('customer2')->where(['id' => [2, 3]])->orderBy('id')->all(); + $this->assertTrue($orders[0]->customer2->orders2 === $orders); + $this->assertTrue($orders[1]->customer2->orders2 === $orders); + + $orders = Order::find()->with('customer2')->where(['id' => [2, 3]])->orderBy('id')->asArray()->all(); + $this->assertTrue($orders[0]['customer2']['orders2'][0]['id'] === $orders[0]['id']); + $this->assertTrue($orders[0]['customer2']['orders2'][1]['id'] === $orders[1]['id']); + $this->assertTrue($orders[1]['customer2']['orders2'][0]['id'] === $orders[0]['id']); + $this->assertTrue($orders[1]['customer2']['orders2'][1]['id'] === $orders[1]['id']); + } } diff --git a/tests/unit/framework/db/BatchQueryResultTest.php b/tests/unit/framework/db/BatchQueryResultTest.php index 7c8043ac4d2..15bb8b49b46 100644 --- a/tests/unit/framework/db/BatchQueryResultTest.php +++ b/tests/unit/framework/db/BatchQueryResultTest.php @@ -19,118 +19,118 @@ */ class BatchQueryResultTest extends DatabaseTestCase { - public function setUp() - { - parent::setUp(); - ActiveRecord::$db = $this->getConnection(); - } + public function setUp() + { + parent::setUp(); + ActiveRecord::$db = $this->getConnection(); + } - public function testQuery() - { - $db = $this->getConnection(); + public function testQuery() + { + $db = $this->getConnection(); - // initialize property test - $query = new Query(); - $query->from('tbl_customer')->orderBy('id'); - $result = $query->batch(2, $db); - $this->assertTrue($result instanceof BatchQueryResult); - $this->assertEquals(2, $result->batchSize); - $this->assertTrue($result->query === $query); + // initialize property test + $query = new Query(); + $query->from('tbl_customer')->orderBy('id'); + $result = $query->batch(2, $db); + $this->assertTrue($result instanceof BatchQueryResult); + $this->assertEquals(2, $result->batchSize); + $this->assertTrue($result->query === $query); - // normal query - $query = new Query(); - $query->from('tbl_customer')->orderBy('id'); - $allRows = []; - $batch = $query->batch(2, $db); - foreach ($batch as $rows) { - $allRows = array_merge($allRows, $rows); - } - $this->assertEquals(3, count($allRows)); - $this->assertEquals('user1', $allRows[0]['name']); - $this->assertEquals('user2', $allRows[1]['name']); - $this->assertEquals('user3', $allRows[2]['name']); - // rewind - $allRows = []; - foreach ($batch as $rows) { - $allRows = array_merge($allRows, $rows); - } - $this->assertEquals(3, count($allRows)); - // reset - $batch->reset(); + // normal query + $query = new Query(); + $query->from('tbl_customer')->orderBy('id'); + $allRows = []; + $batch = $query->batch(2, $db); + foreach ($batch as $rows) { + $allRows = array_merge($allRows, $rows); + } + $this->assertEquals(3, count($allRows)); + $this->assertEquals('user1', $allRows[0]['name']); + $this->assertEquals('user2', $allRows[1]['name']); + $this->assertEquals('user3', $allRows[2]['name']); + // rewind + $allRows = []; + foreach ($batch as $rows) { + $allRows = array_merge($allRows, $rows); + } + $this->assertEquals(3, count($allRows)); + // reset + $batch->reset(); - // empty query - $query = new Query(); - $query->from('tbl_customer')->where(['id' => 100]); - $allRows = []; - $batch = $query->batch(2, $db); - foreach ($batch as $rows) { - $allRows = array_merge($allRows, $rows); - } - $this->assertEquals(0, count($allRows)); + // empty query + $query = new Query(); + $query->from('tbl_customer')->where(['id' => 100]); + $allRows = []; + $batch = $query->batch(2, $db); + foreach ($batch as $rows) { + $allRows = array_merge($allRows, $rows); + } + $this->assertEquals(0, count($allRows)); - // query with index - $query = new Query(); - $query->from('tbl_customer')->indexBy('name'); - $allRows = []; - foreach ($query->batch(2, $db) as $rows) { - $allRows = array_merge($allRows, $rows); - } - $this->assertEquals(3, count($allRows)); - $this->assertEquals('address1', $allRows['user1']['address']); - $this->assertEquals('address2', $allRows['user2']['address']); - $this->assertEquals('address3', $allRows['user3']['address']); + // query with index + $query = new Query(); + $query->from('tbl_customer')->indexBy('name'); + $allRows = []; + foreach ($query->batch(2, $db) as $rows) { + $allRows = array_merge($allRows, $rows); + } + $this->assertEquals(3, count($allRows)); + $this->assertEquals('address1', $allRows['user1']['address']); + $this->assertEquals('address2', $allRows['user2']['address']); + $this->assertEquals('address3', $allRows['user3']['address']); - // each - $query = new Query(); - $query->from('tbl_customer')->orderBy('id'); - $allRows = []; - foreach ($query->each(100, $db) as $rows) { - $allRows[] = $rows; - } - $this->assertEquals(3, count($allRows)); - $this->assertEquals('user1', $allRows[0]['name']); - $this->assertEquals('user2', $allRows[1]['name']); - $this->assertEquals('user3', $allRows[2]['name']); + // each + $query = new Query(); + $query->from('tbl_customer')->orderBy('id'); + $allRows = []; + foreach ($query->each(100, $db) as $rows) { + $allRows[] = $rows; + } + $this->assertEquals(3, count($allRows)); + $this->assertEquals('user1', $allRows[0]['name']); + $this->assertEquals('user2', $allRows[1]['name']); + $this->assertEquals('user3', $allRows[2]['name']); - // each with key - $query = new Query(); - $query->from('tbl_customer')->orderBy('id')->indexBy('name'); - $allRows = []; - foreach ($query->each(100, $db) as $key => $row) { - $allRows[$key] = $row; - } - $this->assertEquals(3, count($allRows)); - $this->assertEquals('address1', $allRows['user1']['address']); - $this->assertEquals('address2', $allRows['user2']['address']); - $this->assertEquals('address3', $allRows['user3']['address']); - } + // each with key + $query = new Query(); + $query->from('tbl_customer')->orderBy('id')->indexBy('name'); + $allRows = []; + foreach ($query->each(100, $db) as $key => $row) { + $allRows[$key] = $row; + } + $this->assertEquals(3, count($allRows)); + $this->assertEquals('address1', $allRows['user1']['address']); + $this->assertEquals('address2', $allRows['user2']['address']); + $this->assertEquals('address3', $allRows['user3']['address']); + } - public function testActiveQuery() - { - $db = $this->getConnection(); + public function testActiveQuery() + { + $db = $this->getConnection(); - $query = Customer::find()->orderBy('id'); - $customers = []; - foreach ($query->batch(2, $db) as $models) { - $customers = array_merge($customers, $models); - } - $this->assertEquals(3, count($customers)); - $this->assertEquals('user1', $customers[0]->name); - $this->assertEquals('user2', $customers[1]->name); - $this->assertEquals('user3', $customers[2]->name); + $query = Customer::find()->orderBy('id'); + $customers = []; + foreach ($query->batch(2, $db) as $models) { + $customers = array_merge($customers, $models); + } + $this->assertEquals(3, count($customers)); + $this->assertEquals('user1', $customers[0]->name); + $this->assertEquals('user2', $customers[1]->name); + $this->assertEquals('user3', $customers[2]->name); - // batch with eager loading - $query = Customer::find()->with('orders')->orderBy('id'); - $customers = []; - foreach ($query->batch(2, $db) as $models) { - $customers = array_merge($customers, $models); - foreach ($models as $model) { - $this->assertTrue($model->isRelationPopulated('orders')); - } - } - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, count($customers[0]->orders)); - $this->assertEquals(2, count($customers[1]->orders)); - $this->assertEquals(0, count($customers[2]->orders)); - } + // batch with eager loading + $query = Customer::find()->with('orders')->orderBy('id'); + $customers = []; + foreach ($query->batch(2, $db) as $models) { + $customers = array_merge($customers, $models); + foreach ($models as $model) { + $this->assertTrue($model->isRelationPopulated('orders')); + } + } + $this->assertEquals(3, count($customers)); + $this->assertEquals(1, count($customers[0]->orders)); + $this->assertEquals(2, count($customers[1]->orders)); + $this->assertEquals(0, count($customers[2]->orders)); + } } diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index 64eb2df0e76..dc908736c11 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -10,269 +10,269 @@ */ class CommandTest extends DatabaseTestCase { - public function testConstruct() - { - $db = $this->getConnection(false); - - // null - $command = $db->createCommand(); - $this->assertEquals(null, $command->sql); - - // string - $sql = 'SELECT * FROM tbl_customer'; - $command = $db->createCommand($sql); - $this->assertEquals($sql, $command->sql); - } - - public function testGetSetSql() - { - $db = $this->getConnection(false); - - $sql = 'SELECT * FROM tbl_customer'; - $command = $db->createCommand($sql); - $this->assertEquals($sql, $command->sql); - - $sql2 = 'SELECT * FROM tbl_order'; - $command->sql = $sql2; - $this->assertEquals($sql2, $command->sql); - } - - public function testAutoQuoting() - { - $db = $this->getConnection(false); - - $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; - $command = $db->createCommand($sql); - $this->assertEquals("SELECT `id`, `t`.`name` FROM `tbl_customer` t", $command->sql); - } - - public function testPrepareCancel() - { - $db = $this->getConnection(false); - - $command = $db->createCommand('SELECT * FROM tbl_customer'); - $this->assertEquals(null, $command->pdoStatement); - $command->prepare(); - $this->assertNotEquals(null, $command->pdoStatement); - $command->cancel(); - $this->assertEquals(null, $command->pdoStatement); - } - - public function testExecute() - { - $db = $this->getConnection(); - - $sql = 'INSERT INTO tbl_customer(email, name , address) VALUES (\'user4@example.com\', \'user4\', \'address4\')'; - $command = $db->createCommand($sql); - $this->assertEquals(1, $command->execute()); - - $sql = 'SELECT COUNT(*) FROM tbl_customer WHERE name =\'user4\''; - $command = $db->createCommand($sql); - $this->assertEquals(1, $command->queryScalar()); - - $command = $db->createCommand('bad SQL'); - $this->setExpectedException('\yii\db\Exception'); - $command->execute(); - } - - public function testQuery() - { - $db = $this->getConnection(); - - // query - $sql = 'SELECT * FROM tbl_customer'; - $reader = $db->createCommand($sql)->query(); - $this->assertTrue($reader instanceof DataReader); - - // queryAll - $rows = $db->createCommand('SELECT * FROM tbl_customer')->queryAll(); - $this->assertEquals(3, count($rows)); - $row = $rows[2]; - $this->assertEquals(3, $row['id']); - $this->assertEquals('user3', $row['name']); - - $rows = $db->createCommand('SELECT * FROM tbl_customer WHERE id=10')->queryAll(); - $this->assertEquals([], $rows); - - // queryOne - $sql = 'SELECT * FROM tbl_customer ORDER BY id'; - $row = $db->createCommand($sql)->queryOne(); - $this->assertEquals(1, $row['id']); - $this->assertEquals('user1', $row['name']); - - $sql = 'SELECT * FROM tbl_customer ORDER BY id'; - $command = $db->createCommand($sql); - $command->prepare(); - $row = $command->queryOne(); - $this->assertEquals(1, $row['id']); - $this->assertEquals('user1', $row['name']); - - $sql = 'SELECT * FROM tbl_customer WHERE id=10'; - $command = $db->createCommand($sql); - $this->assertFalse($command->queryOne()); - - // queryColumn - $sql = 'SELECT * FROM tbl_customer'; - $column = $db->createCommand($sql)->queryColumn(); - $this->assertEquals(range(1, 3), $column); - - $command = $db->createCommand('SELECT id FROM tbl_customer WHERE id=10'); - $this->assertEquals([], $command->queryColumn()); - - // queryScalar - $sql = 'SELECT * FROM tbl_customer ORDER BY id'; - $this->assertEquals($db->createCommand($sql)->queryScalar(), 1); - - $sql = 'SELECT id FROM tbl_customer ORDER BY id'; - $command = $db->createCommand($sql); - $command->prepare(); - $this->assertEquals(1, $command->queryScalar()); - - $command = $db->createCommand('SELECT id FROM tbl_customer WHERE id=10'); - $this->assertFalse($command->queryScalar()); - - $command = $db->createCommand('bad SQL'); - $this->setExpectedException('\yii\db\Exception'); - $command->query(); - } - - public function testBindParamValue() - { - $db = $this->getConnection(); - - // bindParam - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; - $command = $db->createCommand($sql); - $email = 'user4@example.com'; - $name = 'user4'; - $address = 'address4'; - $command->bindParam(':email', $email); - $command->bindParam(':name', $name); - $command->bindParam(':address', $address); - $command->execute(); - - $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; - $command = $db->createCommand($sql); - $command->bindParam(':email', $email); - $this->assertEquals($name, $command->queryScalar()); - - $sql = 'INSERT INTO tbl_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, :blob_col, :numeric_col, :bool_col)'; - $command = $db->createCommand($sql); - $intCol = 123; - $charCol = 'abc'; - $floatCol = 1.23; - $blobCol = "\x10\x11\x12"; - $numericCol = '1.23'; - $boolCol = false; - $command->bindParam(':int_col', $intCol); - $command->bindParam(':char_col', $charCol); - $command->bindParam(':float_col', $floatCol); - $command->bindParam(':blob_col', $blobCol); - $command->bindParam(':numeric_col', $numericCol); - $command->bindParam(':bool_col', $boolCol); - $this->assertEquals(1, $command->execute()); - - $sql = 'SELECT * FROM tbl_type'; - $row = $db->createCommand($sql)->queryOne(); - $this->assertEquals($intCol, $row['int_col']); - $this->assertEquals($charCol, $row['char_col']); - $this->assertEquals($floatCol, $row['float_col']); - $this->assertEquals($blobCol, $row['blob_col']); - $this->assertEquals($numericCol, $row['numeric_col']); - - // bindValue - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; - $command = $db->createCommand($sql); - $command->bindValue(':email', 'user5@example.com'); - $command->execute(); - - $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; - $command = $db->createCommand($sql); - $command->bindValue(':name', 'user5'); - $this->assertEquals('user5@example.com', $command->queryScalar()); - } - - public function testFetchMode() - { - $db = $this->getConnection(); - - // default: FETCH_ASSOC - $sql = 'SELECT * FROM tbl_customer'; - $command = $db->createCommand($sql); - $result = $command->queryOne(); - $this->assertTrue(is_array($result) && isset($result['id'])); - - // FETCH_OBJ, customized via fetchMode property - $sql = 'SELECT * FROM tbl_customer'; - $command = $db->createCommand($sql); - $command->fetchMode = \PDO::FETCH_OBJ; - $result = $command->queryOne(); - $this->assertTrue(is_object($result)); - - // FETCH_NUM, customized in query method - $sql = 'SELECT * FROM tbl_customer'; - $command = $db->createCommand($sql); - $result = $command->queryOne([], \PDO::FETCH_NUM); - $this->assertTrue(is_array($result) && isset($result[0])); - } - - public function testInsert() - { - } - - public function testUpdate() - { - } - - public function testDelete() - { - } - - public function testCreateTable() - { - } - - public function testRenameTable() - { - } - - public function testDropTable() - { - } - - public function testTruncateTable() - { - } - - public function testAddColumn() - { - } - - public function testDropColumn() - { - } - - public function testRenameColumn() - { - } - - public function testAlterColumn() - { - } - - public function testAddForeignKey() - { - } - - public function testDropForeignKey() - { - } - - public function testCreateIndex() - { - } - - public function testDropIndex() - { - } + public function testConstruct() + { + $db = $this->getConnection(false); + + // null + $command = $db->createCommand(); + $this->assertEquals(null, $command->sql); + + // string + $sql = 'SELECT * FROM tbl_customer'; + $command = $db->createCommand($sql); + $this->assertEquals($sql, $command->sql); + } + + public function testGetSetSql() + { + $db = $this->getConnection(false); + + $sql = 'SELECT * FROM tbl_customer'; + $command = $db->createCommand($sql); + $this->assertEquals($sql, $command->sql); + + $sql2 = 'SELECT * FROM tbl_order'; + $command->sql = $sql2; + $this->assertEquals($sql2, $command->sql); + } + + public function testAutoQuoting() + { + $db = $this->getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT `id`, `t`.`name` FROM `tbl_customer` t", $command->sql); + } + + public function testPrepareCancel() + { + $db = $this->getConnection(false); + + $command = $db->createCommand('SELECT * FROM tbl_customer'); + $this->assertEquals(null, $command->pdoStatement); + $command->prepare(); + $this->assertNotEquals(null, $command->pdoStatement); + $command->cancel(); + $this->assertEquals(null, $command->pdoStatement); + } + + public function testExecute() + { + $db = $this->getConnection(); + + $sql = 'INSERT INTO tbl_customer(email, name , address) VALUES (\'user4@example.com\', \'user4\', \'address4\')'; + $command = $db->createCommand($sql); + $this->assertEquals(1, $command->execute()); + + $sql = 'SELECT COUNT(*) FROM tbl_customer WHERE name =\'user4\''; + $command = $db->createCommand($sql); + $this->assertEquals(1, $command->queryScalar()); + + $command = $db->createCommand('bad SQL'); + $this->setExpectedException('\yii\db\Exception'); + $command->execute(); + } + + public function testQuery() + { + $db = $this->getConnection(); + + // query + $sql = 'SELECT * FROM tbl_customer'; + $reader = $db->createCommand($sql)->query(); + $this->assertTrue($reader instanceof DataReader); + + // queryAll + $rows = $db->createCommand('SELECT * FROM tbl_customer')->queryAll(); + $this->assertEquals(3, count($rows)); + $row = $rows[2]; + $this->assertEquals(3, $row['id']); + $this->assertEquals('user3', $row['name']); + + $rows = $db->createCommand('SELECT * FROM tbl_customer WHERE id=10')->queryAll(); + $this->assertEquals([], $rows); + + // queryOne + $sql = 'SELECT * FROM tbl_customer ORDER BY id'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals(1, $row['id']); + $this->assertEquals('user1', $row['name']); + + $sql = 'SELECT * FROM tbl_customer ORDER BY id'; + $command = $db->createCommand($sql); + $command->prepare(); + $row = $command->queryOne(); + $this->assertEquals(1, $row['id']); + $this->assertEquals('user1', $row['name']); + + $sql = 'SELECT * FROM tbl_customer WHERE id=10'; + $command = $db->createCommand($sql); + $this->assertFalse($command->queryOne()); + + // queryColumn + $sql = 'SELECT * FROM tbl_customer'; + $column = $db->createCommand($sql)->queryColumn(); + $this->assertEquals(range(1, 3), $column); + + $command = $db->createCommand('SELECT id FROM tbl_customer WHERE id=10'); + $this->assertEquals([], $command->queryColumn()); + + // queryScalar + $sql = 'SELECT * FROM tbl_customer ORDER BY id'; + $this->assertEquals($db->createCommand($sql)->queryScalar(), 1); + + $sql = 'SELECT id FROM tbl_customer ORDER BY id'; + $command = $db->createCommand($sql); + $command->prepare(); + $this->assertEquals(1, $command->queryScalar()); + + $command = $db->createCommand('SELECT id FROM tbl_customer WHERE id=10'); + $this->assertFalse($command->queryScalar()); + + $command = $db->createCommand('bad SQL'); + $this->setExpectedException('\yii\db\Exception'); + $command->query(); + } + + public function testBindParamValue() + { + $db = $this->getConnection(); + + // bindParam + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); + $command->execute(); + + $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; + $command = $db->createCommand($sql); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); + + $sql = 'INSERT INTO tbl_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, :blob_col, :numeric_col, :bool_col)'; + $command = $db->createCommand($sql); + $intCol = 123; + $charCol = 'abc'; + $floatCol = 1.23; + $blobCol = "\x10\x11\x12"; + $numericCol = '1.23'; + $boolCol = false; + $command->bindParam(':int_col', $intCol); + $command->bindParam(':char_col', $charCol); + $command->bindParam(':float_col', $floatCol); + $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':numeric_col', $numericCol); + $command->bindParam(':bool_col', $boolCol); + $this->assertEquals(1, $command->execute()); + + $sql = 'SELECT * FROM tbl_type'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals($intCol, $row['int_col']); + $this->assertEquals($charCol, $row['char_col']); + $this->assertEquals($floatCol, $row['float_col']); + $this->assertEquals($blobCol, $row['blob_col']); + $this->assertEquals($numericCol, $row['numeric_col']); + + // bindValue + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; + $command = $db->createCommand($sql); + $command->bindValue(':email', 'user5@example.com'); + $command->execute(); + + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; + $command = $db->createCommand($sql); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); + } + + public function testFetchMode() + { + $db = $this->getConnection(); + + // default: FETCH_ASSOC + $sql = 'SELECT * FROM tbl_customer'; + $command = $db->createCommand($sql); + $result = $command->queryOne(); + $this->assertTrue(is_array($result) && isset($result['id'])); + + // FETCH_OBJ, customized via fetchMode property + $sql = 'SELECT * FROM tbl_customer'; + $command = $db->createCommand($sql); + $command->fetchMode = \PDO::FETCH_OBJ; + $result = $command->queryOne(); + $this->assertTrue(is_object($result)); + + // FETCH_NUM, customized in query method + $sql = 'SELECT * FROM tbl_customer'; + $command = $db->createCommand($sql); + $result = $command->queryOne([], \PDO::FETCH_NUM); + $this->assertTrue(is_array($result) && isset($result[0])); + } + + public function testInsert() + { + } + + public function testUpdate() + { + } + + public function testDelete() + { + } + + public function testCreateTable() + { + } + + public function testRenameTable() + { + } + + public function testDropTable() + { + } + + public function testTruncateTable() + { + } + + public function testAddColumn() + { + } + + public function testDropColumn() + { + } + + public function testRenameColumn() + { + } + + public function testAlterColumn() + { + } + + public function testAddForeignKey() + { + } + + public function testDropForeignKey() + { + } + + public function testCreateIndex() + { + } + + public function testDropIndex() + { + } } diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index 04c5d53db04..bbc421591f3 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -10,71 +10,71 @@ */ class ConnectionTest extends DatabaseTestCase { - public function testConstruct() - { - $connection = $this->getConnection(false); - $params = $this->database; + public function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->database; - $this->assertEquals($params['dsn'], $connection->dsn); - $this->assertEquals($params['username'], $connection->username); - $this->assertEquals($params['password'], $connection->password); - } + $this->assertEquals($params['dsn'], $connection->dsn); + $this->assertEquals($params['username'], $connection->username); + $this->assertEquals($params['password'], $connection->password); + } - public function testOpenClose() - { - $connection = $this->getConnection(false, false); + public function testOpenClose() + { + $connection = $this->getConnection(false, false); - $this->assertFalse($connection->isActive); - $this->assertEquals(null, $connection->pdo); + $this->assertFalse($connection->isActive); + $this->assertEquals(null, $connection->pdo); - $connection->open(); - $this->assertTrue($connection->isActive); - $this->assertTrue($connection->pdo instanceof \PDO); + $connection->open(); + $this->assertTrue($connection->isActive); + $this->assertTrue($connection->pdo instanceof \PDO); - $connection->close(); - $this->assertFalse($connection->isActive); - $this->assertEquals(null, $connection->pdo); + $connection->close(); + $this->assertFalse($connection->isActive); + $this->assertEquals(null, $connection->pdo); - $connection = new Connection; - $connection->dsn = 'unknown::memory:'; - $this->setExpectedException('yii\db\Exception'); - $connection->open(); - } + $connection = new Connection; + $connection->dsn = 'unknown::memory:'; + $this->setExpectedException('yii\db\Exception'); + $connection->open(); + } - public function testGetDriverName() - { - $connection = $this->getConnection(false, false); - $this->assertEquals($this->driverName, $connection->driverName); - } + public function testGetDriverName() + { + $connection = $this->getConnection(false, false); + $this->assertEquals($this->driverName, $connection->driverName); + } - public function testQuoteValue() - { - $connection = $this->getConnection(false); - $this->assertEquals(123, $connection->quoteValue(123)); - $this->assertEquals("'string'", $connection->quoteValue('string')); - $this->assertEquals("'It\\'s interesting'", $connection->quoteValue("It's interesting")); - } + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It\\'s interesting'", $connection->quoteValue("It's interesting")); + } - public function testQuoteTableName() - { - $connection = $this->getConnection(false); - $this->assertEquals('`table`', $connection->quoteTableName('table')); - $this->assertEquals('`table`', $connection->quoteTableName('`table`')); - $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table')); - $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`')); - $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); - $this->assertEquals('(table)', $connection->quoteTableName('(table)')); - } + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('`table`', $connection->quoteTableName('table')); + $this->assertEquals('`table`', $connection->quoteTableName('`table`')); + $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table')); + $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } - public function testQuoteColumnName() - { - $connection = $this->getConnection(false); - $this->assertEquals('`column`', $connection->quoteColumnName('column')); - $this->assertEquals('`column`', $connection->quoteColumnName('`column`')); - $this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.column')); - $this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.`column`')); - $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); - $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); - $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); - } + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('`column`', $connection->quoteColumnName('column')); + $this->assertEquals('`column`', $connection->quoteColumnName('`column`')); + $this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.column')); + $this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.`column`')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } } diff --git a/tests/unit/framework/db/DatabaseTestCase.php b/tests/unit/framework/db/DatabaseTestCase.php index f751e0f506d..8bb677b8d6b 100644 --- a/tests/unit/framework/db/DatabaseTestCase.php +++ b/tests/unit/framework/db/DatabaseTestCase.php @@ -6,63 +6,64 @@ abstract class DatabaseTestCase extends TestCase { - protected $database; - protected $driverName = 'mysql'; - /** - * @var Connection - */ - protected $db; + protected $database; + protected $driverName = 'mysql'; + /** + * @var Connection + */ + protected $db; - protected function setUp() - { - parent::setUp(); - $databases = $this->getParam('databases'); - $this->database = $databases[$this->driverName]; - $pdo_database = 'pdo_'.$this->driverName; + protected function setUp() + { + parent::setUp(); + $databases = $this->getParam('databases'); + $this->database = $databases[$this->driverName]; + $pdo_database = 'pdo_'.$this->driverName; - if (!extension_loaded('pdo') || !extension_loaded($pdo_database)) { - $this->markTestSkipped('pdo and '.$pdo_database.' extension are required.'); - } - $this->mockApplication(); - } + if (!extension_loaded('pdo') || !extension_loaded($pdo_database)) { + $this->markTestSkipped('pdo and '.$pdo_database.' extension are required.'); + } + $this->mockApplication(); + } - protected function tearDown() - { - if ($this->db) { - $this->db->close(); - } - $this->destroyApplication(); - } + protected function tearDown() + { + if ($this->db) { + $this->db->close(); + } + $this->destroyApplication(); + } - /** - * @param boolean $reset whether to clean up the test database - * @param boolean $open whether to open and populate test database - * @return \yii\db\Connection - */ - public function getConnection($reset = true, $open = true) - { - if (!$reset && $this->db) { - return $this->db; - } - $db = new \yii\db\Connection; - $db->dsn = $this->database['dsn']; - if (isset($this->database['username'])) { - $db->username = $this->database['username']; - $db->password = $this->database['password']; - } - if (isset($this->database['attributes'])) { - $db->attributes = $this->database['attributes']; - } - if ($open) { - $db->open(); - $lines = explode(';', file_get_contents($this->database['fixture'])); - foreach ($lines as $line) { - if (trim($line) !== '') { - $db->pdo->exec($line); - } - } - } - $this->db = $db; - return $db; - } + /** + * @param boolean $reset whether to clean up the test database + * @param boolean $open whether to open and populate test database + * @return \yii\db\Connection + */ + public function getConnection($reset = true, $open = true) + { + if (!$reset && $this->db) { + return $this->db; + } + $db = new \yii\db\Connection; + $db->dsn = $this->database['dsn']; + if (isset($this->database['username'])) { + $db->username = $this->database['username']; + $db->password = $this->database['password']; + } + if (isset($this->database['attributes'])) { + $db->attributes = $this->database['attributes']; + } + if ($open) { + $db->open(); + $lines = explode(';', file_get_contents($this->database['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + $this->db = $db; + + return $db; + } } diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php index 4a6b1c10fbb..593edcea3e5 100644 --- a/tests/unit/framework/db/QueryBuilderTest.php +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -16,227 +16,227 @@ */ class QueryBuilderTest extends DatabaseTestCase { - /** - * @throws \Exception - * @return QueryBuilder - */ - protected function getQueryBuilder() - { - switch ($this->driverName) { - case 'mysql': - return new MysqlQueryBuilder($this->getConnection()); - case 'sqlite': - return new SqliteQueryBuilder($this->getConnection()); - case 'mssql': - return new MssqlQueryBuilder($this->getConnection()); - case 'pgsql': - return new PgsqlQueryBuilder($this->getConnection()); - case 'cubrid': - return new CubridQueryBuilder($this->getConnection()); - } - throw new \Exception('Test is not implemented for ' . $this->driverName); - } - - /** - * this is not used as a dataprovider for testGetColumnType to speed up the test - * when used as dataprovider every single line will cause a reconnect with the database which is not needed here - */ - public function columnTypes() - { - return [ - [Schema::TYPE_PK, 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'], - [Schema::TYPE_PK . '(8)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY'], - [Schema::TYPE_PK . ' CHECK (value > 5)', 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], - [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], - [Schema::TYPE_STRING, 'varchar(255)'], - [Schema::TYPE_STRING . '(32)', 'varchar(32)'], - [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], - [Schema::TYPE_TEXT, 'text'], - [Schema::TYPE_TEXT . '(255)', 'text'], - [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'], - [Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'], - [Schema::TYPE_SMALLINT, 'smallint(6)'], - [Schema::TYPE_SMALLINT . '(8)', 'smallint(8)'], - [Schema::TYPE_INTEGER, 'int(11)'], - [Schema::TYPE_INTEGER . '(8)', 'int(8)'], - [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int(11) CHECK (value > 5)'], - [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int(8) CHECK (value > 5)'], - [Schema::TYPE_INTEGER . ' NOT NULL', 'int(11) NOT NULL'], - [Schema::TYPE_BIGINT, 'bigint(20)'], - [Schema::TYPE_BIGINT . '(8)', 'bigint(8)'], - [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint(20) CHECK (value > 5)'], - [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint(8) CHECK (value > 5)'], - [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint(20) NOT NULL'], - [Schema::TYPE_FLOAT, 'float'], - [Schema::TYPE_FLOAT . '(16,5)', 'float'], - [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'], - [Schema::TYPE_DECIMAL, 'decimal(10,0)'], - [Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'], - [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'], - [Schema::TYPE_DATETIME, 'datetime'], - [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'], - [Schema::TYPE_TIMESTAMP, 'timestamp'], - [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], - [Schema::TYPE_TIME, 'time'], - [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], - [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], - [Schema::TYPE_DATE, 'date'], - [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], - [Schema::TYPE_BINARY, 'blob'], - [Schema::TYPE_BOOLEAN, 'tinyint(1)'], - [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'tinyint(1) NOT NULL DEFAULT 1'], - [Schema::TYPE_MONEY, 'decimal(19,4)'], - [Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'], - [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'], - ]; - } - - public function testGetColumnType() - { - $qb = $this->getQueryBuilder(); - foreach ($this->columnTypes() as $item) { - list ($column, $expected) = $item; - $this->assertEquals($expected, $qb->getColumnType($column)); - } - } - - public function testAddDropPrimaryKey() - { - $tableName = 'tbl_constraints'; - $pkeyName = $tableName . "_pkey"; - - // ADD - $qb = $this->getQueryBuilder(); - $qb->db->createCommand()->addPrimaryKey($pkeyName, $tableName, ['id'])->execute(); - $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); - $this->assertEquals(1, count($tableSchema->primaryKey)); - - //DROP - $qb->db->createCommand()->dropPrimaryKey($pkeyName, $tableName)->execute(); - $qb = $this->getQueryBuilder(); // resets the schema - $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); - $this->assertEquals(0, count($tableSchema->primaryKey)); - } - - /* qiangxue: the following tests are commented because they vary by different DB drivers. need a better test scheme. - public function testBuildWhereExists() - { - $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE EXISTS (SELECT `1` FROM `Website` `w`)"; - $expectedQueryParams = null; - - $subQuery = new Query(); - $subQuery->select('1') - ->from('Website w'); - - $query = new Query(); - $query->select('id') - ->from('TotalExample t') - ->where(['exists', $subQuery]); - - list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $actualQueryParams); - } - - public function testBuildWhereNotExists() - { - $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE NOT EXISTS (SELECT `1` FROM `Website` `w`)"; - $expectedQueryParams = null; - - $subQuery = new Query(); - $subQuery->select('1') - ->from('Website w'); - - $query = new Query(); - $query->select('id') - ->from('TotalExample t') - ->where(['not exists', $subQuery]); - - list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $actualQueryParams); - } - - public function testBuildWhereExistsWithParameters() - { - $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE (EXISTS (SELECT `1` FROM `Website` `w` WHERE (w.id = t.website_id) AND (w.merchant_id = :merchant_id))) AND (t.some_column = :some_value)"; - $expectedQueryParams = [':some_value' => "asd", ':merchant_id' => 6]; - - $subQuery = new Query(); - $subQuery->select('1') - ->from('Website w') - ->where('w.id = t.website_id') - ->andWhere('w.merchant_id = :merchant_id', [':merchant_id' => 6]); - - $query = new Query(); - $query->select('id') - ->from('TotalExample t') - ->where(['exists', $subQuery]) - ->andWhere('t.some_column = :some_value', [':some_value' => "asd"]); - - list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $queryParams); - } - - public function testBuildWhereExistsWithArrayParameters() - { - $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE (EXISTS (SELECT `1` FROM `Website` `w` WHERE (w.id = t.website_id) AND ((`w`.`merchant_id`=:qp0) AND (`w`.`user_id`=:qp1)))) AND (`t`.`some_column`=:qp2)"; - $expectedQueryParams = [':qp0' => 6, ':qp1' => 210, ':qp2' => 'asd']; - - $subQuery = new Query(); - $subQuery->select('1') - ->from('Website w') - ->where('w.id = t.website_id') - ->andWhere(['w.merchant_id' => 6, 'w.user_id' => '210']); - - $query = new Query(); - $query->select('id') - ->from('TotalExample t') - ->where(['exists', $subQuery]) - ->andWhere(['t.some_column' => "asd"]); - - list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $queryParams); - } - */ - - /* - This test contains three select queries connected with UNION and UNION ALL constructions. - It could be useful to use "phpunit --group=db --filter testBuildUnion" command for run it. - - public function testBuildUnion() - { - $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t1` WHERE (w > 0) AND (x < 2) UNION ( SELECT `id` FROM `TotalTotalExample` `t2` WHERE w > 5 ) UNION ALL ( SELECT `id` FROM `TotalTotalExample` `t3` WHERE w = 3 )"; - $query = new Query(); - $secondQuery = new Query(); - $secondQuery->select('id') - ->from('TotalTotalExample t2') - ->where('w > 5'); - $thirdQuery = new Query(); - $thirdQuery->select('id') - ->from('TotalTotalExample t3') - ->where('w = 3'); - $query->select('id') - ->from('TotalExample t1') - ->where(['and', 'w > 0', 'x < 2']) - ->union($secondQuery) - ->union($thirdQuery, TRUE); - list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - }*/ + /** + * @throws \Exception + * @return QueryBuilder + */ + protected function getQueryBuilder() + { + switch ($this->driverName) { + case 'mysql': + return new MysqlQueryBuilder($this->getConnection()); + case 'sqlite': + return new SqliteQueryBuilder($this->getConnection()); + case 'mssql': + return new MssqlQueryBuilder($this->getConnection()); + case 'pgsql': + return new PgsqlQueryBuilder($this->getConnection()); + case 'cubrid': + return new CubridQueryBuilder($this->getConnection()); + } + throw new \Exception('Test is not implemented for ' . $this->driverName); + } + + /** + * this is not used as a dataprovider for testGetColumnType to speed up the test + * when used as dataprovider every single line will cause a reconnect with the database which is not needed here + */ + public function columnTypes() + { + return [ + [Schema::TYPE_PK, 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'], + [Schema::TYPE_PK . '(8)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY'], + [Schema::TYPE_PK . ' CHECK (value > 5)', 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_STRING, 'varchar(255)'], + [Schema::TYPE_STRING . '(32)', 'varchar(32)'], + [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], + [Schema::TYPE_TEXT, 'text'], + [Schema::TYPE_TEXT . '(255)', 'text'], + [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'], + [Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'], + [Schema::TYPE_SMALLINT, 'smallint(6)'], + [Schema::TYPE_SMALLINT . '(8)', 'smallint(8)'], + [Schema::TYPE_INTEGER, 'int(11)'], + [Schema::TYPE_INTEGER . '(8)', 'int(8)'], + [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int(11) CHECK (value > 5)'], + [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int(8) CHECK (value > 5)'], + [Schema::TYPE_INTEGER . ' NOT NULL', 'int(11) NOT NULL'], + [Schema::TYPE_BIGINT, 'bigint(20)'], + [Schema::TYPE_BIGINT . '(8)', 'bigint(8)'], + [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint(20) CHECK (value > 5)'], + [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint(8) CHECK (value > 5)'], + [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint(20) NOT NULL'], + [Schema::TYPE_FLOAT, 'float'], + [Schema::TYPE_FLOAT . '(16,5)', 'float'], + [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'], + [Schema::TYPE_DECIMAL, 'decimal(10,0)'], + [Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'], + [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'], + [Schema::TYPE_DATETIME, 'datetime'], + [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'], + [Schema::TYPE_TIMESTAMP, 'timestamp'], + [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIME, 'time'], + [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], + [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], + [Schema::TYPE_DATE, 'date'], + [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], + [Schema::TYPE_BINARY, 'blob'], + [Schema::TYPE_BOOLEAN, 'tinyint(1)'], + [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'tinyint(1) NOT NULL DEFAULT 1'], + [Schema::TYPE_MONEY, 'decimal(19,4)'], + [Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'], + [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'], + ]; + } + + public function testGetColumnType() + { + $qb = $this->getQueryBuilder(); + foreach ($this->columnTypes() as $item) { + list ($column, $expected) = $item; + $this->assertEquals($expected, $qb->getColumnType($column)); + } + } + + public function testAddDropPrimaryKey() + { + $tableName = 'tbl_constraints'; + $pkeyName = $tableName . "_pkey"; + + // ADD + $qb = $this->getQueryBuilder(); + $qb->db->createCommand()->addPrimaryKey($pkeyName, $tableName, ['id'])->execute(); + $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); + $this->assertEquals(1, count($tableSchema->primaryKey)); + + //DROP + $qb->db->createCommand()->dropPrimaryKey($pkeyName, $tableName)->execute(); + $qb = $this->getQueryBuilder(); // resets the schema + $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); + $this->assertEquals(0, count($tableSchema->primaryKey)); + } + + /* qiangxue: the following tests are commented because they vary by different DB drivers. need a better test scheme. + public function testBuildWhereExists() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE EXISTS (SELECT `1` FROM `Website` `w`)"; + $expectedQueryParams = null; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w'); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['exists', $subQuery]); + + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } + + public function testBuildWhereNotExists() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE NOT EXISTS (SELECT `1` FROM `Website` `w`)"; + $expectedQueryParams = null; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w'); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['not exists', $subQuery]); + + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } + + public function testBuildWhereExistsWithParameters() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE (EXISTS (SELECT `1` FROM `Website` `w` WHERE (w.id = t.website_id) AND (w.merchant_id = :merchant_id))) AND (t.some_column = :some_value)"; + $expectedQueryParams = [':some_value' => "asd", ':merchant_id' => 6]; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w') + ->where('w.id = t.website_id') + ->andWhere('w.merchant_id = :merchant_id', [':merchant_id' => 6]); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['exists', $subQuery]) + ->andWhere('t.some_column = :some_value', [':some_value' => "asd"]); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $queryParams); + } + + public function testBuildWhereExistsWithArrayParameters() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE (EXISTS (SELECT `1` FROM `Website` `w` WHERE (w.id = t.website_id) AND ((`w`.`merchant_id`=:qp0) AND (`w`.`user_id`=:qp1)))) AND (`t`.`some_column`=:qp2)"; + $expectedQueryParams = [':qp0' => 6, ':qp1' => 210, ':qp2' => 'asd']; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w') + ->where('w.id = t.website_id') + ->andWhere(['w.merchant_id' => 6, 'w.user_id' => '210']); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['exists', $subQuery]) + ->andWhere(['t.some_column' => "asd"]); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $queryParams); + } + */ + + /* + This test contains three select queries connected with UNION and UNION ALL constructions. + It could be useful to use "phpunit --group=db --filter testBuildUnion" command for run it. + + public function testBuildUnion() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t1` WHERE (w > 0) AND (x < 2) UNION ( SELECT `id` FROM `TotalTotalExample` `t2` WHERE w > 5 ) UNION ALL ( SELECT `id` FROM `TotalTotalExample` `t3` WHERE w = 3 )"; + $query = new Query(); + $secondQuery = new Query(); + $secondQuery->select('id') + ->from('TotalTotalExample t2') + ->where('w > 5'); + $thirdQuery = new Query(); + $thirdQuery->select('id') + ->from('TotalTotalExample t3') + ->where('w = 3'); + $query->select('id') + ->from('TotalExample t1') + ->where(['and', 'w > 0', 'x < 2']) + ->union($secondQuery) + ->union($thirdQuery, TRUE); + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + }*/ } diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 199b6a269a9..f6187ae384f 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -10,117 +10,117 @@ */ class QueryTest extends DatabaseTestCase { - public function testSelect() - { - // default - $query = new Query; - $query->select('*'); - $this->assertEquals(['*'], $query->select); - $this->assertNull($query->distinct); - $this->assertEquals(null, $query->selectOption); - - $query = new Query; - $query->select('id, name', 'something')->distinct(true); - $this->assertEquals(['id', 'name'], $query->select); - $this->assertTrue($query->distinct); - $this->assertEquals('something', $query->selectOption); - } - - public function testFrom() - { - $query = new Query; - $query->from('tbl_user'); - $this->assertEquals(['tbl_user'], $query->from); - } - - public function testWhere() - { - $query = new Query; - $query->where('id = :id', [':id' => 1]); - $this->assertEquals('id = :id', $query->where); - $this->assertEquals([':id' => 1], $query->params); - - $query->andWhere('name = :name', [':name' => 'something']); - $this->assertEquals(['and', 'id = :id', 'name = :name'], $query->where); - $this->assertEquals([':id' => 1, ':name' => 'something'], $query->params); - - $query->orWhere('age = :age', [':age' => '30']); - $this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->where); - $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); - } - - public function testJoin() - { - } - - public function testGroup() - { - $query = new Query; - $query->groupBy('team'); - $this->assertEquals(['team'], $query->groupBy); - - $query->addGroupBy('company'); - $this->assertEquals(['team', 'company'], $query->groupBy); - - $query->addGroupBy('age'); - $this->assertEquals(['team', 'company', 'age'], $query->groupBy); - } - - public function testHaving() - { - $query = new Query; - $query->having('id = :id', [':id' => 1]); - $this->assertEquals('id = :id', $query->having); - $this->assertEquals([':id' => 1], $query->params); - - $query->andHaving('name = :name', [':name' => 'something']); - $this->assertEquals(['and', 'id = :id', 'name = :name'], $query->having); - $this->assertEquals([':id' => 1, ':name' => 'something'], $query->params); - - $query->orHaving('age = :age', [':age' => '30']); - $this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->having); - $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); - } - - public function testOrder() - { - $query = new Query; - $query->orderBy('team'); - $this->assertEquals(['team' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('company'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('age'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); - - $query->addOrderBy(['age' => SORT_DESC]); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); - - $query->addOrderBy('age ASC, company DESC'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); - } - - public function testLimitOffset() - { - $query = new Query; - $query->limit(10)->offset(5); - $this->assertEquals(10, $query->limit); - $this->assertEquals(5, $query->offset); - } - - public function testUnion() - { - } - - public function testOne() - { - $db = $this->getConnection(); - - $result = (new Query)->from('tbl_customer')->where(['status' => 2])->one($db); - $this->assertEquals('user3', $result['name']); - - $result = (new Query)->from('tbl_customer')->where(['status' => 3])->one($db); - $this->assertFalse($result); - } + public function testSelect() + { + // default + $query = new Query; + $query->select('*'); + $this->assertEquals(['*'], $query->select); + $this->assertNull($query->distinct); + $this->assertEquals(null, $query->selectOption); + + $query = new Query; + $query->select('id, name', 'something')->distinct(true); + $this->assertEquals(['id', 'name'], $query->select); + $this->assertTrue($query->distinct); + $this->assertEquals('something', $query->selectOption); + } + + public function testFrom() + { + $query = new Query; + $query->from('tbl_user'); + $this->assertEquals(['tbl_user'], $query->from); + } + + public function testWhere() + { + $query = new Query; + $query->where('id = :id', [':id' => 1]); + $this->assertEquals('id = :id', $query->where); + $this->assertEquals([':id' => 1], $query->params); + + $query->andWhere('name = :name', [':name' => 'something']); + $this->assertEquals(['and', 'id = :id', 'name = :name'], $query->where); + $this->assertEquals([':id' => 1, ':name' => 'something'], $query->params); + + $query->orWhere('age = :age', [':age' => '30']); + $this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->where); + $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); + } + + public function testJoin() + { + } + + public function testGroup() + { + $query = new Query; + $query->groupBy('team'); + $this->assertEquals(['team'], $query->groupBy); + + $query->addGroupBy('company'); + $this->assertEquals(['team', 'company'], $query->groupBy); + + $query->addGroupBy('age'); + $this->assertEquals(['team', 'company', 'age'], $query->groupBy); + } + + public function testHaving() + { + $query = new Query; + $query->having('id = :id', [':id' => 1]); + $this->assertEquals('id = :id', $query->having); + $this->assertEquals([':id' => 1], $query->params); + + $query->andHaving('name = :name', [':name' => 'something']); + $this->assertEquals(['and', 'id = :id', 'name = :name'], $query->having); + $this->assertEquals([':id' => 1, ':name' => 'something'], $query->params); + + $query->orHaving('age = :age', [':age' => '30']); + $this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->having); + $this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params); + } + + public function testOrder() + { + $query = new Query; + $query->orderBy('team'); + $this->assertEquals(['team' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('company'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); + + $query->addOrderBy('age'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); + + $query->addOrderBy(['age' => SORT_DESC]); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); + + $query->addOrderBy('age ASC, company DESC'); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); + } + + public function testLimitOffset() + { + $query = new Query; + $query->limit(10)->offset(5); + $this->assertEquals(10, $query->limit); + $this->assertEquals(5, $query->offset); + } + + public function testUnion() + { + } + + public function testOne() + { + $db = $this->getConnection(); + + $result = (new Query)->from('tbl_customer')->where(['status' => 2])->one($db); + $this->assertEquals('user3', $result['name']); + + $result = (new Query)->from('tbl_customer')->where(['status' => 3])->one($db); + $this->assertFalse($result); + } } diff --git a/tests/unit/framework/db/SchemaTest.php b/tests/unit/framework/db/SchemaTest.php index 89811a1a3b4..8dcb2de8782 100644 --- a/tests/unit/framework/db/SchemaTest.php +++ b/tests/unit/framework/db/SchemaTest.php @@ -11,83 +11,83 @@ */ class SchemaTest extends DatabaseTestCase { - public function testGetTableNames() - { - /** @var Schema $schema */ - $schema = $this->getConnection()->schema; + public function testGetTableNames() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; - $tables = $schema->getTableNames(); - $this->assertTrue(in_array('tbl_customer', $tables)); - $this->assertTrue(in_array('tbl_category', $tables)); - $this->assertTrue(in_array('tbl_item', $tables)); - $this->assertTrue(in_array('tbl_order', $tables)); - $this->assertTrue(in_array('tbl_order_item', $tables)); - $this->assertTrue(in_array('tbl_type', $tables)); - } + $tables = $schema->getTableNames(); + $this->assertTrue(in_array('tbl_customer', $tables)); + $this->assertTrue(in_array('tbl_category', $tables)); + $this->assertTrue(in_array('tbl_item', $tables)); + $this->assertTrue(in_array('tbl_order', $tables)); + $this->assertTrue(in_array('tbl_order_item', $tables)); + $this->assertTrue(in_array('tbl_type', $tables)); + } - public function testGetTableSchemas() - { - /** @var Schema $schema */ - $schema = $this->getConnection()->schema; + public function testGetTableSchemas() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; - $tables = $schema->getTableSchemas(); - $this->assertEquals(count($schema->getTableNames()), count($tables)); - foreach ($tables as $table) { - $this->assertInstanceOf('yii\db\TableSchema', $table); - } - } + $tables = $schema->getTableSchemas(); + $this->assertEquals(count($schema->getTableNames()), count($tables)); + foreach ($tables as $table) { + $this->assertInstanceOf('yii\db\TableSchema', $table); + } + } - public function testGetNonExistingTableSchema() - { - $this->assertNull($this->getConnection()->schema->getTableSchema('nonexisting_table')); - } + public function testGetNonExistingTableSchema() + { + $this->assertNull($this->getConnection()->schema->getTableSchema('nonexisting_table')); + } - public function testSchemaCache() - { - /** @var Schema $schema */ - $schema = $this->getConnection()->schema; + public function testSchemaCache() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; - $schema->db->enableSchemaCache = true; - $schema->db->schemaCache = new FileCache(); - $noCacheTable = $schema->getTableSchema('tbl_type', true); - $cachedTable = $schema->getTableSchema('tbl_type', true); - $this->assertEquals($noCacheTable, $cachedTable); - } + $schema->db->enableSchemaCache = true; + $schema->db->schemaCache = new FileCache(); + $noCacheTable = $schema->getTableSchema('tbl_type', true); + $cachedTable = $schema->getTableSchema('tbl_type', true); + $this->assertEquals($noCacheTable, $cachedTable); + } - public function testCompositeFk() - { - /** @var Schema $schema */ - $schema = $this->getConnection()->schema; + public function testCompositeFk() + { + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; - $table = $schema->getTableSchema('tbl_composite_fk'); + $table = $schema->getTableSchema('tbl_composite_fk'); - $this->assertCount(1, $table->foreignKeys); - $this->assertTrue(isset($table->foreignKeys[0])); - $this->assertEquals('tbl_order_item', $table->foreignKeys[0][0]); - $this->assertEquals('order_id', $table->foreignKeys[0]['order_id']); - $this->assertEquals('item_id', $table->foreignKeys[0]['item_id']); - } + $this->assertCount(1, $table->foreignKeys); + $this->assertTrue(isset($table->foreignKeys[0])); + $this->assertEquals('tbl_order_item', $table->foreignKeys[0][0]); + $this->assertEquals('order_id', $table->foreignKeys[0]['order_id']); + $this->assertEquals('item_id', $table->foreignKeys[0]['item_id']); + } - public function testGetPDOType() - { - $values = [ - [null, \PDO::PARAM_NULL], - ['', \PDO::PARAM_STR], - ['hello', \PDO::PARAM_STR], - [0, \PDO::PARAM_INT], - [1, \PDO::PARAM_INT], - [1337, \PDO::PARAM_INT], - [true, \PDO::PARAM_BOOL], - [false, \PDO::PARAM_BOOL], - [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], - ]; + public function testGetPDOType() + { + $values = [ + [null, \PDO::PARAM_NULL], + ['', \PDO::PARAM_STR], + ['hello', \PDO::PARAM_STR], + [0, \PDO::PARAM_INT], + [1, \PDO::PARAM_INT], + [1337, \PDO::PARAM_INT], + [true, \PDO::PARAM_BOOL], + [false, \PDO::PARAM_BOOL], + [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], + ]; - /** @var Schema $schema */ - $schema = $this->getConnection()->schema; + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; - foreach ($values as $value) { - $this->assertEquals($value[1], $schema->getPdoType($value[0])); - } - fclose($fp); - } + foreach ($values as $value) { + $this->assertEquals($value[1], $schema->getPdoType($value[0])); + } + fclose($fp); + } } diff --git a/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php b/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php index 191442af001..f01e69f0877 100644 --- a/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php +++ b/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php @@ -10,5 +10,5 @@ */ class CubridActiveDataProviderTest extends ActiveDataProviderTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; } diff --git a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php index dd48f443422..ff362f3235d 100644 --- a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php +++ b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php @@ -9,5 +9,5 @@ */ class CubridActiveRecordTest extends ActiveRecordTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; } diff --git a/tests/unit/framework/db/cubrid/CubridCommandTest.php b/tests/unit/framework/db/cubrid/CubridCommandTest.php index 45d3c1c3d14..50c993a887d 100644 --- a/tests/unit/framework/db/cubrid/CubridCommandTest.php +++ b/tests/unit/framework/db/cubrid/CubridCommandTest.php @@ -9,71 +9,71 @@ */ class CubridCommandTest extends CommandTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; - public function testBindParamValue() - { - $db = $this->getConnection(); + public function testBindParamValue() + { + $db = $this->getConnection(); - // bindParam - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; - $command = $db->createCommand($sql); - $email = 'user4@example.com'; - $name = 'user4'; - $address = 'address4'; - $command->bindParam(':email', $email); - $command->bindParam(':name', $name); - $command->bindParam(':address', $address); - $command->execute(); + // bindParam + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); + $command->execute(); - $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; - $command = $db->createCommand($sql); - $command->bindParam(':email', $email); - $this->assertEquals($name, $command->queryScalar()); + $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; + $command = $db->createCommand($sql); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); - $sql = "INSERT INTO tbl_type (int_col, char_col, char_col2, enum_col, float_col, blob_col, numeric_col) VALUES (:int_col, '', :char_col, :enum_col, :float_col, CHAR_TO_BLOB(:blob_col), :numeric_col)"; - $command = $db->createCommand($sql); - $intCol = 123; - $charCol = 'abc'; - $enumCol = 'a'; - $floatCol = 1.23; - $blobCol = "\x10\x11\x12"; - $numericCol = '1.23'; - $command->bindParam(':int_col', $intCol); - $command->bindParam(':char_col', $charCol); - $command->bindParam(':enum_col', $enumCol); - $command->bindParam(':float_col', $floatCol); - $command->bindParam(':blob_col', $blobCol); - $command->bindParam(':numeric_col', $numericCol); - $this->assertEquals(1, $command->execute()); + $sql = "INSERT INTO tbl_type (int_col, char_col, char_col2, enum_col, float_col, blob_col, numeric_col) VALUES (:int_col, '', :char_col, :enum_col, :float_col, CHAR_TO_BLOB(:blob_col), :numeric_col)"; + $command = $db->createCommand($sql); + $intCol = 123; + $charCol = 'abc'; + $enumCol = 'a'; + $floatCol = 1.23; + $blobCol = "\x10\x11\x12"; + $numericCol = '1.23'; + $command->bindParam(':int_col', $intCol); + $command->bindParam(':char_col', $charCol); + $command->bindParam(':enum_col', $enumCol); + $command->bindParam(':float_col', $floatCol); + $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':numeric_col', $numericCol); + $this->assertEquals(1, $command->execute()); - $sql = 'SELECT * FROM tbl_type'; - $row = $db->createCommand($sql)->queryOne(); - $this->assertEquals($intCol, $row['int_col']); - $this->assertEquals($enumCol, $row['enum_col']); - $this->assertEquals($charCol, $row['char_col2']); - $this->assertEquals($floatCol, $row['float_col']); - $this->assertEquals($blobCol, fread($row['blob_col'], 3)); - $this->assertEquals($numericCol, $row['numeric_col']); + $sql = 'SELECT * FROM tbl_type'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals($intCol, $row['int_col']); + $this->assertEquals($enumCol, $row['enum_col']); + $this->assertEquals($charCol, $row['char_col2']); + $this->assertEquals($floatCol, $row['float_col']); + $this->assertEquals($blobCol, fread($row['blob_col'], 3)); + $this->assertEquals($numericCol, $row['numeric_col']); - // bindValue - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; - $command = $db->createCommand($sql); - $command->bindValue(':email', 'user5@example.com'); - $command->execute(); + // bindValue + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; + $command = $db->createCommand($sql); + $command->bindValue(':email', 'user5@example.com'); + $command->execute(); - $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; - $command = $db->createCommand($sql); - $command->bindValue(':name', 'user5'); - $this->assertEquals('user5@example.com', $command->queryScalar()); - } + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; + $command = $db->createCommand($sql); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); + } - public function testAutoQuoting() - { - $db = $this->getConnection(false); + public function testAutoQuoting() + { + $db = $this->getConnection(false); - $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; - $command = $db->createCommand($sql); - $this->assertEquals('SELECT "id", "t"."name" FROM "tbl_customer" t', $command->sql); - } + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals('SELECT "id", "t"."name" FROM "tbl_customer" t', $command->sql); + } } diff --git a/tests/unit/framework/db/cubrid/CubridConnectionTest.php b/tests/unit/framework/db/cubrid/CubridConnectionTest.php index 4cd6e20da21..9b432d0a97b 100644 --- a/tests/unit/framework/db/cubrid/CubridConnectionTest.php +++ b/tests/unit/framework/db/cubrid/CubridConnectionTest.php @@ -9,36 +9,36 @@ */ class CubridConnectionTest extends ConnectionTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; - public function testQuoteValue() - { - $connection = $this->getConnection(false); - $this->assertEquals(123, $connection->quoteValue(123)); - $this->assertEquals("'string'", $connection->quoteValue('string')); - $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); - } + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } - public function testQuoteTableName() - { - $connection = $this->getConnection(false); - $this->assertEquals('"table"', $connection->quoteTableName('table')); - $this->assertEquals('"table"', $connection->quoteTableName('"table"')); - $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.table')); - $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema."table"')); - $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); - $this->assertEquals('(table)', $connection->quoteTableName('(table)')); - } + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"table"', $connection->quoteTableName('table')); + $this->assertEquals('"table"', $connection->quoteTableName('"table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.table')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema."table"')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } - public function testQuoteColumnName() - { - $connection = $this->getConnection(false); - $this->assertEquals('"column"', $connection->quoteColumnName('column')); - $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); - $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); - $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); - $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); - $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); - $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); - } + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } } diff --git a/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php b/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php index 86937589b11..fb61a948c4d 100644 --- a/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php +++ b/tests/unit/framework/db/cubrid/CubridQueryBuilderTest.php @@ -11,72 +11,72 @@ */ class CubridQueryBuilderTest extends QueryBuilderTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; - /** - * this is not used as a dataprovider for testGetColumnType to speed up the test - * when used as dataprovider every single line will cause a reconnect with the database which is not needed here - */ - public function columnTypes() - { - return [ - [Schema::TYPE_PK, 'int NOT NULL AUTO_INCREMENT PRIMARY KEY'], - [Schema::TYPE_PK . '(8)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY'], - [Schema::TYPE_PK . ' CHECK (value > 5)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], - [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], - [Schema::TYPE_STRING, 'varchar(255)'], - [Schema::TYPE_STRING . '(32)', 'varchar(32)'], - [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], - [Schema::TYPE_TEXT, 'varchar'], - [Schema::TYPE_TEXT . '(255)', 'varchar'], - [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'varchar CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'varchar CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . ' NOT NULL', 'varchar NOT NULL'], - [Schema::TYPE_TEXT . '(255) NOT NULL', 'varchar NOT NULL'], - [Schema::TYPE_SMALLINT, 'smallint'], - [Schema::TYPE_SMALLINT . '(8)', 'smallint'], - [Schema::TYPE_INTEGER, 'int'], - [Schema::TYPE_INTEGER . '(8)', 'int'], - [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int CHECK (value > 5)'], - [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int CHECK (value > 5)'], - [Schema::TYPE_INTEGER . ' NOT NULL', 'int NOT NULL'], - [Schema::TYPE_BIGINT, 'bigint'], - [Schema::TYPE_BIGINT . '(8)', 'bigint'], - [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], - [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], - [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], - [Schema::TYPE_FLOAT, 'float(7)'], - [Schema::TYPE_FLOAT . '(16)', 'float(16)'], - [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float(7) CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . '(16) CHECK (value > 5.6)', 'float(16) CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . ' NOT NULL', 'float(7) NOT NULL'], - [Schema::TYPE_DECIMAL, 'decimal(10,0)'], - [Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'], - [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'], - [Schema::TYPE_DATETIME, 'datetime'], - [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'], - [Schema::TYPE_TIMESTAMP, 'timestamp'], - [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], - [Schema::TYPE_TIME, 'time'], - [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], - [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], - [Schema::TYPE_DATE, 'date'], - [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], - [Schema::TYPE_BINARY, 'blob'], - [Schema::TYPE_BOOLEAN, 'smallint'], - [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'smallint NOT NULL DEFAULT 1'], - [Schema::TYPE_MONEY, 'decimal(19,4)'], - [Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'], - [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'], - ]; - } + /** + * this is not used as a dataprovider for testGetColumnType to speed up the test + * when used as dataprovider every single line will cause a reconnect with the database which is not needed here + */ + public function columnTypes() + { + return [ + [Schema::TYPE_PK, 'int NOT NULL AUTO_INCREMENT PRIMARY KEY'], + [Schema::TYPE_PK . '(8)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY'], + [Schema::TYPE_PK . ' CHECK (value > 5)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_STRING, 'varchar(255)'], + [Schema::TYPE_STRING . '(32)', 'varchar(32)'], + [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], + [Schema::TYPE_TEXT, 'varchar'], + [Schema::TYPE_TEXT . '(255)', 'varchar'], + [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'varchar CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'varchar CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . ' NOT NULL', 'varchar NOT NULL'], + [Schema::TYPE_TEXT . '(255) NOT NULL', 'varchar NOT NULL'], + [Schema::TYPE_SMALLINT, 'smallint'], + [Schema::TYPE_SMALLINT . '(8)', 'smallint'], + [Schema::TYPE_INTEGER, 'int'], + [Schema::TYPE_INTEGER . '(8)', 'int'], + [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int CHECK (value > 5)'], + [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int CHECK (value > 5)'], + [Schema::TYPE_INTEGER . ' NOT NULL', 'int NOT NULL'], + [Schema::TYPE_BIGINT, 'bigint'], + [Schema::TYPE_BIGINT . '(8)', 'bigint'], + [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], + [Schema::TYPE_FLOAT, 'float(7)'], + [Schema::TYPE_FLOAT . '(16)', 'float(16)'], + [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float(7) CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . '(16) CHECK (value > 5.6)', 'float(16) CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . ' NOT NULL', 'float(7) NOT NULL'], + [Schema::TYPE_DECIMAL, 'decimal(10,0)'], + [Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'], + [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'], + [Schema::TYPE_DATETIME, 'datetime'], + [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'], + [Schema::TYPE_TIMESTAMP, 'timestamp'], + [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIME, 'time'], + [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], + [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], + [Schema::TYPE_DATE, 'date'], + [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], + [Schema::TYPE_BINARY, 'blob'], + [Schema::TYPE_BOOLEAN, 'smallint'], + [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'smallint NOT NULL DEFAULT 1'], + [Schema::TYPE_MONEY, 'decimal(19,4)'], + [Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'], + [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'], + ]; + } } diff --git a/tests/unit/framework/db/cubrid/CubridQueryTest.php b/tests/unit/framework/db/cubrid/CubridQueryTest.php index b7c9009028e..50fe0d88871 100644 --- a/tests/unit/framework/db/cubrid/CubridQueryTest.php +++ b/tests/unit/framework/db/cubrid/CubridQueryTest.php @@ -9,5 +9,5 @@ */ class CubridQueryTest extends QueryTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; } diff --git a/tests/unit/framework/db/cubrid/CubridSchemaTest.php b/tests/unit/framework/db/cubrid/CubridSchemaTest.php index 21e799fec63..4f9ee920bb8 100644 --- a/tests/unit/framework/db/cubrid/CubridSchemaTest.php +++ b/tests/unit/framework/db/cubrid/CubridSchemaTest.php @@ -9,28 +9,28 @@ */ class CubridSchemaTest extends SchemaTest { - public $driverName = 'cubrid'; + public $driverName = 'cubrid'; - public function testGetPDOType() - { - $values = [ - [null, \PDO::PARAM_NULL], - ['', \PDO::PARAM_STR], - ['hello', \PDO::PARAM_STR], - [0, \PDO::PARAM_INT], - [1, \PDO::PARAM_INT], - [1337, \PDO::PARAM_INT], - [true, \PDO::PARAM_INT], - [false, \PDO::PARAM_INT], - [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], - ]; + public function testGetPDOType() + { + $values = [ + [null, \PDO::PARAM_NULL], + ['', \PDO::PARAM_STR], + ['hello', \PDO::PARAM_STR], + [0, \PDO::PARAM_INT], + [1, \PDO::PARAM_INT], + [1337, \PDO::PARAM_INT], + [true, \PDO::PARAM_INT], + [false, \PDO::PARAM_INT], + [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], + ]; - /** @var Schema $schema */ - $schema = $this->getConnection()->schema; + /** @var Schema $schema */ + $schema = $this->getConnection()->schema; - foreach ($values as $value) { - $this->assertEquals($value[1], $schema->getPdoType($value[0])); - } - fclose($fp); - } + foreach ($values as $value) { + $this->assertEquals($value[1], $schema->getPdoType($value[0])); + } + fclose($fp); + } } diff --git a/tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php b/tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php index 001b660ca8b..e9f49943c6c 100644 --- a/tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php +++ b/tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php @@ -10,5 +10,5 @@ */ class MssqlActiveDataProviderTest extends ActiveDataProviderTest { - public $driverName = 'sqlsrv'; + public $driverName = 'sqlsrv'; } diff --git a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php index c21efc696b9..afb1bea19cc 100644 --- a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php +++ b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php @@ -10,5 +10,5 @@ */ class MssqlActiveRecordTest extends ActiveRecordTest { - protected $driverName = 'sqlsrv'; + protected $driverName = 'sqlsrv'; } diff --git a/tests/unit/framework/db/mssql/MssqlCommandTest.php b/tests/unit/framework/db/mssql/MssqlCommandTest.php index 86f7f45cc37..3d37d333d1c 100644 --- a/tests/unit/framework/db/mssql/MssqlCommandTest.php +++ b/tests/unit/framework/db/mssql/MssqlCommandTest.php @@ -10,75 +10,75 @@ */ class MssqlCommandTest extends CommandTest { - protected $driverName = 'sqlsrv'; + protected $driverName = 'sqlsrv'; - public function testAutoQuoting() - { - $db = $this->getConnection(false); + public function testAutoQuoting() + { + $db = $this->getConnection(false); - $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; - $command = $db->createCommand($sql); - $this->assertEquals("SELECT [id], [t].[name] FROM [tbl_customer] t", $command->sql); - } + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT [id], [t].[name] FROM [tbl_customer] t", $command->sql); + } - public function testPrepareCancel() - { - $this->markTestSkipped('MSSQL driver does not support this feature.'); - } + public function testPrepareCancel() + { + $this->markTestSkipped('MSSQL driver does not support this feature.'); + } - public function testBindParamValue() - { - $db = $this->getConnection(); + public function testBindParamValue() + { + $db = $this->getConnection(); - // bindParam - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; - $command = $db->createCommand($sql); - $email = 'user4@example.com'; - $name = 'user4'; - $address = 'address4'; - $command->bindParam(':email', $email); - $command->bindParam(':name', $name); - $command->bindParam(':address', $address); - $command->execute(); + // bindParam + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); + $command->execute(); - $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; - $command = $db->createCommand($sql); - $command->bindParam(':email', $email); - $this->assertEquals($name, $command->queryScalar()); + $sql = 'SELECT name FROM tbl_customer WHERE email=:email'; + $command = $db->createCommand($sql); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); - $sql = 'INSERT INTO tbl_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, CONVERT([varbinary], :blob_col), :numeric_col, :bool_col)'; - $command = $db->createCommand($sql); - $intCol = 123; - $charCol = 'abc'; - $floatCol = 1.23; - $blobCol = "\x10\x11\x12"; - $numericCol = '1.23'; - $boolCol = false; - $command->bindParam(':int_col', $intCol); - $command->bindParam(':char_col', $charCol); - $command->bindParam(':float_col', $floatCol); - $command->bindParam(':blob_col', $blobCol); - $command->bindParam(':numeric_col', $numericCol); - $command->bindParam(':bool_col', $boolCol); - $this->assertEquals(1, $command->execute()); + $sql = 'INSERT INTO tbl_type (int_col, char_col, float_col, blob_col, numeric_col, bool_col) VALUES (:int_col, :char_col, :float_col, CONVERT([varbinary], :blob_col), :numeric_col, :bool_col)'; + $command = $db->createCommand($sql); + $intCol = 123; + $charCol = 'abc'; + $floatCol = 1.23; + $blobCol = "\x10\x11\x12"; + $numericCol = '1.23'; + $boolCol = false; + $command->bindParam(':int_col', $intCol); + $command->bindParam(':char_col', $charCol); + $command->bindParam(':float_col', $floatCol); + $command->bindParam(':blob_col', $blobCol); + $command->bindParam(':numeric_col', $numericCol); + $command->bindParam(':bool_col', $boolCol); + $this->assertEquals(1, $command->execute()); - $sql = 'SELECT int_col, char_col, float_col, CONVERT([nvarchar], blob_col) AS blob_col, numeric_col FROM tbl_type'; - $row = $db->createCommand($sql)->queryOne(); - $this->assertEquals($intCol, $row['int_col']); - $this->assertEquals($charCol, trim($row['char_col'])); - $this->assertEquals($floatCol, $row['float_col']); - $this->assertEquals($blobCol, $row['blob_col']); - $this->assertEquals($numericCol, $row['numeric_col']); + $sql = 'SELECT int_col, char_col, float_col, CONVERT([nvarchar], blob_col) AS blob_col, numeric_col FROM tbl_type'; + $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals($intCol, $row['int_col']); + $this->assertEquals($charCol, trim($row['char_col'])); + $this->assertEquals($floatCol, $row['float_col']); + $this->assertEquals($blobCol, $row['blob_col']); + $this->assertEquals($numericCol, $row['numeric_col']); - // bindValue - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; - $command = $db->createCommand($sql); - $command->bindValue(':email', 'user5@example.com'); - $command->execute(); + // bindValue + $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user5\', \'address5\')'; + $command = $db->createCommand($sql); + $command->bindValue(':email', 'user5@example.com'); + $command->execute(); - $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; - $command = $db->createCommand($sql); - $command->bindValue(':name', 'user5'); - $this->assertEquals('user5@example.com', $command->queryScalar()); - } + $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; + $command = $db->createCommand($sql); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); + } } diff --git a/tests/unit/framework/db/mssql/MssqlConnectionTest.php b/tests/unit/framework/db/mssql/MssqlConnectionTest.php index 6531f83ddfc..02f7bfd9d52 100644 --- a/tests/unit/framework/db/mssql/MssqlConnectionTest.php +++ b/tests/unit/framework/db/mssql/MssqlConnectionTest.php @@ -10,36 +10,36 @@ */ class MssqlConnectionTest extends ConnectionTest { - protected $driverName = 'sqlsrv'; + protected $driverName = 'sqlsrv'; - public function testQuoteValue() - { - $connection = $this->getConnection(false); - $this->assertEquals(123, $connection->quoteValue(123)); - $this->assertEquals("'string'", $connection->quoteValue('string')); - $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); - } + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } - public function testQuoteTableName() - { - $connection = $this->getConnection(false); - $this->assertEquals('[table]', $connection->quoteTableName('table')); - $this->assertEquals('[table]', $connection->quoteTableName('[table]')); - $this->assertEquals('[schema].[table]', $connection->quoteTableName('schema.table')); - $this->assertEquals('[schema].[table]', $connection->quoteTableName('schema.[table]')); - $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); - $this->assertEquals('(table)', $connection->quoteTableName('(table)')); - } + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('[table]', $connection->quoteTableName('table')); + $this->assertEquals('[table]', $connection->quoteTableName('[table]')); + $this->assertEquals('[schema].[table]', $connection->quoteTableName('schema.table')); + $this->assertEquals('[schema].[table]', $connection->quoteTableName('schema.[table]')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } - public function testQuoteColumnName() - { - $connection = $this->getConnection(false); - $this->assertEquals('[column]', $connection->quoteColumnName('column')); - $this->assertEquals('[column]', $connection->quoteColumnName('[column]')); - $this->assertEquals('[table].[column]', $connection->quoteColumnName('table.column')); - $this->assertEquals('[table].[column]', $connection->quoteColumnName('table.[column]')); - $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); - $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); - $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); - } + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('[column]', $connection->quoteColumnName('column')); + $this->assertEquals('[column]', $connection->quoteColumnName('[column]')); + $this->assertEquals('[table].[column]', $connection->quoteColumnName('table.column')); + $this->assertEquals('[table].[column]', $connection->quoteColumnName('table.[column]')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } } diff --git a/tests/unit/framework/db/mssql/MssqlQueryBuilderTest.php b/tests/unit/framework/db/mssql/MssqlQueryBuilderTest.php index 56105ef7bd3..987d6a4e272 100644 --- a/tests/unit/framework/db/mssql/MssqlQueryBuilderTest.php +++ b/tests/unit/framework/db/mssql/MssqlQueryBuilderTest.php @@ -9,49 +9,49 @@ * @group db * @group mssql */ -class MSSQLQueryBuilderTest extends QueryBuilderTest +class MssqlQueryBuilderTest extends QueryBuilderTest { - public $driverName = 'sqlsrv'; + public $driverName = 'sqlsrv'; - public function testOffsetLimit() - { - $expectedQuerySql = 'SELECT `id` FROM `exapmle` OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY'; - $expectedQueryParams = null; + public function testOffsetLimit() + { + $expectedQuerySql = 'SELECT `id` FROM `exapmle` OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY'; + $expectedQueryParams = null; - $query = new Query(); - $query->select('id')->from('example')->limit(10)->offset(5); + $query = new Query(); + $query->select('id')->from('example')->limit(10)->offset(5); - list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $actualQueryParams); - } + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } - public function testLimit() - { - $expectedQuerySql = 'SELECT `id` FROM `exapmle` OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY'; - $expectedQueryParams = null; + public function testLimit() + { + $expectedQuerySql = 'SELECT `id` FROM `exapmle` OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY'; + $expectedQueryParams = null; - $query = new Query(); - $query->select('id')->from('example')->limit(10); + $query = new Query(); + $query->select('id')->from('example')->limit(10); - list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $actualQueryParams); - } + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } - public function testOffset() - { - $expectedQuerySql = 'SELECT `id` FROM `exapmle` OFFSET 10 ROWS'; - $expectedQueryParams = null; + public function testOffset() + { + $expectedQuerySql = 'SELECT `id` FROM `exapmle` OFFSET 10 ROWS'; + $expectedQueryParams = null; - $query = new Query(); - $query->select('id')->from('example')->offset(10); + $query = new Query(); + $query->select('id')->from('example')->offset(10); - list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); - $this->assertEquals($expectedQuerySql, $actualQuerySql); - $this->assertEquals($expectedQueryParams, $actualQueryParams); - } + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } } diff --git a/tests/unit/framework/db/mssql/MssqlQueryTest.php b/tests/unit/framework/db/mssql/MssqlQueryTest.php index a2cb019ead6..89830d036dc 100644 --- a/tests/unit/framework/db/mssql/MssqlQueryTest.php +++ b/tests/unit/framework/db/mssql/MssqlQueryTest.php @@ -10,5 +10,5 @@ */ class MssqlQueryTest extends QueryTest { - protected $driverName = 'sqlsrv'; + protected $driverName = 'sqlsrv'; } diff --git a/tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php b/tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php index 674b5646cd9..081f680bc9a 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php @@ -10,5 +10,5 @@ */ class PostgreSQLActiveDataProviderTest extends ActiveDataProviderTest { - public $driverName = 'pgsql'; + public $driverName = 'pgsql'; } diff --git a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php index 1fffad7cd7a..1b2a687b2af 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php @@ -10,5 +10,5 @@ */ class PostgreSQLActiveRecordTest extends ActiveRecordTest { - protected $driverName = 'pgsql'; + protected $driverName = 'pgsql'; } diff --git a/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php index 6ba61036b9d..bdbe547e78a 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php @@ -9,43 +9,43 @@ */ class PostgreSQLConnectionTest extends ConnectionTest { - protected $driverName = 'pgsql'; + protected $driverName = 'pgsql'; - public function testConnection() - { - $this->getConnection(true); - } + public function testConnection() + { + $this->getConnection(true); + } - public function testQuoteValue() - { - $connection = $this->getConnection(false); - $this->assertEquals(123, $connection->quoteValue(123)); - $this->assertEquals("'string'", $connection->quoteValue('string')); - $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); - } + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } - public function testQuoteTableName() - { - $connection = $this->getConnection(false); - $this->assertEquals('"table"', $connection->quoteTableName('table')); - $this->assertEquals('"table"', $connection->quoteTableName('"table"')); - $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.table')); - $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema."table"')); - $this->assertEquals('"schema"."table"', $connection->quoteTableName('"schema"."table"')); - $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); - $this->assertEquals('(table)', $connection->quoteTableName('(table)')); - } + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"table"', $connection->quoteTableName('table')); + $this->assertEquals('"table"', $connection->quoteTableName('"table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.table')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema."table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('"schema"."table"')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } - public function testQuoteColumnName() - { - $connection = $this->getConnection(false); - $this->assertEquals('"column"', $connection->quoteColumnName('column')); - $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); - $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); - $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); - $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table"."column"')); - $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); - $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); - $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); - } + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table"."column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } } diff --git a/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php b/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php index e2a62d0c173..37bbc96d2e1 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php @@ -11,67 +11,67 @@ */ class PostgreSQLQueryBuilderTest extends QueryBuilderTest { - public $driverName = 'pgsql'; - - public function columnTypes() - { - return [ - [Schema::TYPE_PK, 'serial NOT NULL PRIMARY KEY'], - [Schema::TYPE_PK . '(8)', 'serial NOT NULL PRIMARY KEY'], - [Schema::TYPE_PK . ' CHECK (value > 5)', 'serial NOT NULL PRIMARY KEY CHECK (value > 5)'], - [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'serial NOT NULL PRIMARY KEY CHECK (value > 5)'], - [Schema::TYPE_STRING, 'varchar(255)'], - [Schema::TYPE_STRING . '(32)', 'varchar(32)'], - [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], - [Schema::TYPE_TEXT, 'text'], - [Schema::TYPE_TEXT . '(255)', 'text'], - [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'], - [Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'], - [Schema::TYPE_SMALLINT, 'smallint'], - [Schema::TYPE_SMALLINT . '(8)', 'smallint'], - [Schema::TYPE_INTEGER, 'integer'], - [Schema::TYPE_INTEGER . '(8)', 'integer'], - [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'], - [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'], - [Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'], - [Schema::TYPE_BIGINT, 'bigint'], - [Schema::TYPE_BIGINT . '(8)', 'bigint'], - [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], - [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], - [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], - [Schema::TYPE_FLOAT, 'double precision'], - [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . ' NOT NULL', 'double precision NOT NULL'], - [Schema::TYPE_DECIMAL, 'numeric(10,0)'], - [Schema::TYPE_DECIMAL . '(12,4)', 'numeric(12,4)'], - [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'numeric(10,0) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'numeric(12,4) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . ' NOT NULL', 'numeric(10,0) NOT NULL'], - [Schema::TYPE_DATETIME, 'timestamp'], - [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATETIME . ' NOT NULL', 'timestamp NOT NULL'], - [Schema::TYPE_TIMESTAMP, 'timestamp'], - [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], - [Schema::TYPE_TIME, 'time'], - [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], - [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], - [Schema::TYPE_DATE, 'date'], - [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], - [Schema::TYPE_BINARY, 'bytea'], - [Schema::TYPE_BOOLEAN, 'boolean'], - [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'], - [Schema::TYPE_MONEY, 'numeric(19,4)'], - [Schema::TYPE_MONEY . '(16,2)', 'numeric(16,2)'], - [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'numeric(19,4) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'numeric(16,2) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . ' NOT NULL', 'numeric(19,4) NOT NULL'], - ]; - } + public $driverName = 'pgsql'; + + public function columnTypes() + { + return [ + [Schema::TYPE_PK, 'serial NOT NULL PRIMARY KEY'], + [Schema::TYPE_PK . '(8)', 'serial NOT NULL PRIMARY KEY'], + [Schema::TYPE_PK . ' CHECK (value > 5)', 'serial NOT NULL PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'serial NOT NULL PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_STRING, 'varchar(255)'], + [Schema::TYPE_STRING . '(32)', 'varchar(32)'], + [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], + [Schema::TYPE_TEXT, 'text'], + [Schema::TYPE_TEXT . '(255)', 'text'], + [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'], + [Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'], + [Schema::TYPE_SMALLINT, 'smallint'], + [Schema::TYPE_SMALLINT . '(8)', 'smallint'], + [Schema::TYPE_INTEGER, 'integer'], + [Schema::TYPE_INTEGER . '(8)', 'integer'], + [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'], + [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'], + [Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'], + [Schema::TYPE_BIGINT, 'bigint'], + [Schema::TYPE_BIGINT . '(8)', 'bigint'], + [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], + [Schema::TYPE_FLOAT, 'double precision'], + [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . ' NOT NULL', 'double precision NOT NULL'], + [Schema::TYPE_DECIMAL, 'numeric(10,0)'], + [Schema::TYPE_DECIMAL . '(12,4)', 'numeric(12,4)'], + [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'numeric(10,0) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'numeric(12,4) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . ' NOT NULL', 'numeric(10,0) NOT NULL'], + [Schema::TYPE_DATETIME, 'timestamp'], + [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATETIME . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIMESTAMP, 'timestamp'], + [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIME, 'time'], + [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], + [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], + [Schema::TYPE_DATE, 'date'], + [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], + [Schema::TYPE_BINARY, 'bytea'], + [Schema::TYPE_BOOLEAN, 'boolean'], + [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'], + [Schema::TYPE_MONEY, 'numeric(19,4)'], + [Schema::TYPE_MONEY . '(16,2)', 'numeric(16,2)'], + [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'numeric(19,4) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'numeric(16,2) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . ' NOT NULL', 'numeric(19,4) NOT NULL'], + ]; + } } diff --git a/tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php b/tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php index 660b14df5f9..245bc880484 100644 --- a/tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php +++ b/tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php @@ -10,5 +10,5 @@ */ class SqliteActiveDataProviderTest extends ActiveDataProviderTest { - public $driverName = 'sqlite'; + public $driverName = 'sqlite'; } diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php index a689e5d6e52..28f0574f9a7 100644 --- a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -9,5 +9,5 @@ */ class SqliteActiveRecordTest extends ActiveRecordTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; } diff --git a/tests/unit/framework/db/sqlite/SqliteCommandTest.php b/tests/unit/framework/db/sqlite/SqliteCommandTest.php index c8cb35c4f99..3ab24f1a9f2 100644 --- a/tests/unit/framework/db/sqlite/SqliteCommandTest.php +++ b/tests/unit/framework/db/sqlite/SqliteCommandTest.php @@ -9,14 +9,14 @@ */ class SqliteCommandTest extends CommandTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; - public function testAutoQuoting() - { - $db = $this->getConnection(false); + public function testAutoQuoting() + { + $db = $this->getConnection(false); - $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; - $command = $db->createCommand($sql); - $this->assertEquals("SELECT `id`, `t`.`name` FROM `tbl_customer` t", $command->sql); - } + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT `id`, `t`.`name` FROM `tbl_customer` t", $command->sql); + } } diff --git a/tests/unit/framework/db/sqlite/SqliteConnectionTest.php b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php index ea74f814ed8..eb648a7c425 100644 --- a/tests/unit/framework/db/sqlite/SqliteConnectionTest.php +++ b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php @@ -9,40 +9,40 @@ */ class SqliteConnectionTest extends ConnectionTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; - public function testConstruct() - { - $connection = $this->getConnection(false); - $params = $this->database; + public function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->database; - $this->assertEquals($params['dsn'], $connection->dsn); - } + $this->assertEquals($params['dsn'], $connection->dsn); + } - public function testQuoteValue() - { - $connection = $this->getConnection(false); - $this->assertEquals(123, $connection->quoteValue(123)); - $this->assertEquals("'string'", $connection->quoteValue('string')); - $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); - } + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } - public function testQuoteTableName() - { - $connection = $this->getConnection(false); - $this->assertEquals("`table`", $connection->quoteTableName('table')); - $this->assertEquals("`schema`.`table`", $connection->quoteTableName('schema.table')); - $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); - $this->assertEquals('(table)', $connection->quoteTableName('(table)')); - } + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals("`table`", $connection->quoteTableName('table')); + $this->assertEquals("`schema`.`table`", $connection->quoteTableName('schema.table')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } - public function testQuoteColumnName() - { - $connection = $this->getConnection(false); - $this->assertEquals('`column`', $connection->quoteColumnName('column')); - $this->assertEquals("`table`.`column`", $connection->quoteColumnName('table.column')); - $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); - $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); - $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); - } + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('`column`', $connection->quoteColumnName('column')); + $this->assertEquals("`table`.`column`", $connection->quoteColumnName('table.column')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } } diff --git a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php index b9d03b8cdaa..6bebb9be5c8 100644 --- a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php +++ b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php @@ -11,80 +11,80 @@ */ class SqliteQueryBuilderTest extends QueryBuilderTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; - public function columnTypes() - { - return [ - [Schema::TYPE_PK, 'integer PRIMARY KEY AUTOINCREMENT NOT NULL'], - [Schema::TYPE_PK . '(8)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL'], - [Schema::TYPE_PK . ' CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'], - [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'], - [Schema::TYPE_STRING, 'varchar(255)'], - [Schema::TYPE_STRING . '(32)', 'varchar(32)'], - [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], - [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], - [Schema::TYPE_TEXT, 'text'], - [Schema::TYPE_TEXT . '(255)', 'text'], - [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], - [Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'], - [Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'], - [Schema::TYPE_SMALLINT, 'smallint'], - [Schema::TYPE_SMALLINT . '(8)', 'smallint'], - [Schema::TYPE_INTEGER, 'integer'], - [Schema::TYPE_INTEGER . '(8)', 'integer'], - [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'], - [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'], - [Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'], - [Schema::TYPE_BIGINT, 'bigint'], - [Schema::TYPE_BIGINT . '(8)', 'bigint'], - [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], - [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], - [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], - [Schema::TYPE_FLOAT, 'float'], - [Schema::TYPE_FLOAT . '(16,5)', 'float'], - [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], - [Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'], - [Schema::TYPE_DECIMAL, 'decimal(10,0)'], - [Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'], - [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'], - [Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'], - [Schema::TYPE_DATETIME, 'datetime'], - [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'], - [Schema::TYPE_TIMESTAMP, 'timestamp'], - [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], - [Schema::TYPE_TIME, 'time'], - [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], - [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], - [Schema::TYPE_DATE, 'date'], - [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], - [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], - [Schema::TYPE_BINARY, 'blob'], - [Schema::TYPE_BOOLEAN, 'boolean'], - [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'], - [Schema::TYPE_MONEY, 'decimal(19,4)'], - [Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'], - [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'], - [Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'], - ]; - } - - public function testAddDropPrimaryKey() - { - $this->setExpectedException('yii\base\NotSupportedException'); - parent::testAddDropPrimaryKey(); - } + public function columnTypes() + { + return [ + [Schema::TYPE_PK, 'integer PRIMARY KEY AUTOINCREMENT NOT NULL'], + [Schema::TYPE_PK . '(8)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL'], + [Schema::TYPE_PK . ' CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'], + [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'], + [Schema::TYPE_STRING, 'varchar(255)'], + [Schema::TYPE_STRING . '(32)', 'varchar(32)'], + [Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'], + [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], + [Schema::TYPE_TEXT, 'text'], + [Schema::TYPE_TEXT . '(255)', 'text'], + [Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'], + [Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'], + [Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'], + [Schema::TYPE_SMALLINT, 'smallint'], + [Schema::TYPE_SMALLINT . '(8)', 'smallint'], + [Schema::TYPE_INTEGER, 'integer'], + [Schema::TYPE_INTEGER . '(8)', 'integer'], + [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'], + [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'], + [Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'], + [Schema::TYPE_BIGINT, 'bigint'], + [Schema::TYPE_BIGINT . '(8)', 'bigint'], + [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], + [Schema::TYPE_FLOAT, 'float'], + [Schema::TYPE_FLOAT . '(16,5)', 'float'], + [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'], + [Schema::TYPE_DECIMAL, 'decimal(10,0)'], + [Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'], + [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'], + [Schema::TYPE_DATETIME, 'datetime'], + [Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'], + [Schema::TYPE_TIMESTAMP, 'timestamp'], + [Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIME, 'time'], + [Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"], + [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], + [Schema::TYPE_DATE, 'date'], + [Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], + [Schema::TYPE_BINARY, 'blob'], + [Schema::TYPE_BOOLEAN, 'boolean'], + [Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'], + [Schema::TYPE_MONEY, 'decimal(19,4)'], + [Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'], + [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'], + ]; + } - public function testBatchInsert() - { - $sql = $this->getQueryBuilder()->batchInsert('{{tbl_customer}} t', ['t.id', 't.name'], [[1, 'a'], [2, 'b']]); - $this->assertEquals("INSERT INTO {{tbl_customer}} t (`t`.`id`, `t`.`name`) SELECT 1, 'a' UNION ALL 2, 'b'", $sql); - } + public function testAddDropPrimaryKey() + { + $this->setExpectedException('yii\base\NotSupportedException'); + parent::testAddDropPrimaryKey(); + } + + public function testBatchInsert() + { + $sql = $this->getQueryBuilder()->batchInsert('{{tbl_customer}} t', ['t.id', 't.name'], [[1, 'a'], [2, 'b']]); + $this->assertEquals("INSERT INTO {{tbl_customer}} t (`t`.`id`, `t`.`name`) SELECT 1, 'a' UNION ALL 2, 'b'", $sql); + } } diff --git a/tests/unit/framework/db/sqlite/SqliteQueryTest.php b/tests/unit/framework/db/sqlite/SqliteQueryTest.php index f1db36b2684..5c85077b3c1 100644 --- a/tests/unit/framework/db/sqlite/SqliteQueryTest.php +++ b/tests/unit/framework/db/sqlite/SqliteQueryTest.php @@ -9,5 +9,5 @@ */ class SqliteQueryTest extends QueryTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; } diff --git a/tests/unit/framework/db/sqlite/SqliteSchemaTest.php b/tests/unit/framework/db/sqlite/SqliteSchemaTest.php index 260bb4c7dae..1cb6c6d0f51 100644 --- a/tests/unit/framework/db/sqlite/SqliteSchemaTest.php +++ b/tests/unit/framework/db/sqlite/SqliteSchemaTest.php @@ -9,5 +9,5 @@ */ class SqliteSchemaTest extends SchemaTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; } diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index d55b4b0ceed..6a3f6128e19 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -9,30 +9,30 @@ class Post1 { - public $id = 23; - public $title = 'tt'; + public $id = 23; + public $title = 'tt'; } class Post2 extends Object { - public $id = 123; - public $content = 'test'; - private $secret = 's'; - public function getSecret() - { - return $this->secret; - } + public $id = 123; + public $content = 'test'; + private $secret = 's'; + public function getSecret() + { + return $this->secret; + } } class Post3 extends Object { - public $id = 33; - public $subObject; + public $id = 33; + public $subObject; - public function init() - { - $this->subObject = new Post2(); - } + public function init() + { + $this->subObject = new Post2(); + } } /** @@ -40,342 +40,341 @@ public function init() */ class ArrayHelperTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - public function testToArray() - { - $object = new Post1; - $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); - $object = new Post2; - $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); - - $object1 = new Post1; - $object2 = new Post2; - $this->assertEquals([ - get_object_vars($object1), - get_object_vars($object2), - ], ArrayHelper::toArray([ - $object1, - $object2, - ])); - - $object = new Post2; - $this->assertEquals([ - 'id' => 123, - 'secret' => 's', - '_content' => 'test', - 'length' => 4, - ], ArrayHelper::toArray($object, [ - $object->className() => [ - 'id', 'secret', - '_content' => 'content', - 'length' => function ($post) { - return strlen($post->content); - } - ] - ])); - - $object = new Post3(); - $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object, [], false)); - $this->assertEquals([ - 'id' => 33, - 'subObject' => [ - 'id' => 123, - 'content' => 'test', - ], - ], ArrayHelper::toArray($object)); - } - - public function testRemove() - { - $array = ['name' => 'b', 'age' => 3]; - $name = ArrayHelper::remove($array, 'name'); - - $this->assertEquals($name, 'b'); - $this->assertEquals($array, ['age' => 3]); - - $default = ArrayHelper::remove($array, 'nonExisting', 'defaultValue'); - $this->assertEquals('defaultValue', $default); - } - - - public function testMultisort() - { - // single key - $array = [ - ['name' => 'b', 'age' => 3], - ['name' => 'a', 'age' => 1], - ['name' => 'c', 'age' => 2], - ]; - ArrayHelper::multisort($array, 'name'); - $this->assertEquals(['name' => 'a', 'age' => 1], $array[0]); - $this->assertEquals(['name' => 'b', 'age' => 3], $array[1]); - $this->assertEquals(['name' => 'c', 'age' => 2], $array[2]); - - // multiple keys - $array = [ - ['name' => 'b', 'age' => 3], - ['name' => 'a', 'age' => 2], - ['name' => 'a', 'age' => 1], - ]; - ArrayHelper::multisort($array, ['name', 'age']); - $this->assertEquals(['name' => 'a', 'age' => 1], $array[0]); - $this->assertEquals(['name' => 'a', 'age' => 2], $array[1]); - $this->assertEquals(['name' => 'b', 'age' => 3], $array[2]); - - // case-insensitive - $array = [ - ['name' => 'a', 'age' => 3], - ['name' => 'b', 'age' => 2], - ['name' => 'B', 'age' => 4], - ['name' => 'A', 'age' => 1], - ]; - - ArrayHelper::multisort($array, ['name', 'age'], SORT_ASC, [SORT_STRING, SORT_REGULAR]); - $this->assertEquals(['name' => 'A', 'age' => 1], $array[0]); - $this->assertEquals(['name' => 'B', 'age' => 4], $array[1]); - $this->assertEquals(['name' => 'a', 'age' => 3], $array[2]); - $this->assertEquals(['name' => 'b', 'age' => 2], $array[3]); - - ArrayHelper::multisort($array, ['name', 'age'], SORT_ASC, [SORT_STRING | SORT_FLAG_CASE, SORT_REGULAR]); - $this->assertEquals(['name' => 'A', 'age' => 1], $array[0]); - $this->assertEquals(['name' => 'a', 'age' => 3], $array[1]); - $this->assertEquals(['name' => 'b', 'age' => 2], $array[2]); - $this->assertEquals(['name' => 'B', 'age' => 4], $array[3]); - } - - public function testMultisortUseSort() - { - // single key - $sort = new Sort([ - 'attributes' => ['name', 'age'], - 'defaultOrder' => ['name' => SORT_ASC], - ]); - $orders = $sort->getOrders(); - - $array = [ - ['name' => 'b', 'age' => 3], - ['name' => 'a', 'age' => 1], - ['name' => 'c', 'age' => 2], - ]; - ArrayHelper::multisort($array, array_keys($orders), array_values($orders)); - $this->assertEquals(['name' => 'a', 'age' => 1], $array[0]); - $this->assertEquals(['name' => 'b', 'age' => 3], $array[1]); - $this->assertEquals(['name' => 'c', 'age' => 2], $array[2]); - - // multiple keys - $sort = new Sort([ - 'attributes' => ['name', 'age'], - 'defaultOrder' => ['name' => SORT_ASC, 'age' => SORT_DESC], - ]); - $orders = $sort->getOrders(); - - $array = [ - ['name' => 'b', 'age' => 3], - ['name' => 'a', 'age' => 2], - ['name' => 'a', 'age' => 1], - ]; - ArrayHelper::multisort($array, array_keys($orders), array_values($orders)); - $this->assertEquals(['name' => 'a', 'age' => 2], $array[0]); - $this->assertEquals(['name' => 'a', 'age' => 1], $array[1]); - $this->assertEquals(['name' => 'b', 'age' => 3], $array[2]); - } - - public function testMerge() - { - $a = [ - 'name' => 'Yii', - 'version' => '1.0', - 'options' => [ - 'namespace' => false, - 'unittest' => false, - ], - 'features' => [ - 'mvc', - ], - ]; - $b = [ - 'version' => '1.1', - 'options' => [ - 'unittest' => true, - ], - 'features' => [ - 'gii', - ], - ]; - $c = [ - 'version' => '2.0', - 'options' => [ - 'namespace' => true, - ], - 'features' => [ - 'debug', - ], - ]; - - $result = ArrayHelper::merge($a, $b, $c); - $expected = [ - 'name' => 'Yii', - 'version' => '2.0', - 'options' => [ - 'namespace' => true, - 'unittest' => true, - ], - 'features' => [ - 'mvc', - 'gii', - 'debug', - ], - ]; - - $this->assertEquals($expected, $result); - } - - public function testIndex() - { - $array = [ - ['id' => '123', 'data' => 'abc'], - ['id' => '345', 'data' => 'def'], - ]; - $result = ArrayHelper::index($array, 'id'); - $this->assertEquals([ - '123' => ['id' => '123', 'data' => 'abc'], - '345' => ['id' => '345', 'data' => 'def'], - ], $result); - - $result = ArrayHelper::index($array, function ($element) { - return $element['data']; - }); - $this->assertEquals([ - 'abc' => ['id' => '123', 'data' => 'abc'], - 'def' => ['id' => '345', 'data' => 'def'], - ], $result); - } - - public function testGetColumn() - { - $array = [ - 'a' => ['id' => '123', 'data' => 'abc'], - 'b' => ['id' => '345', 'data' => 'def'], - ]; - $result = ArrayHelper::getColumn($array, 'id'); - $this->assertEquals(['a' => '123', 'b' => '345'], $result); - $result = ArrayHelper::getColumn($array, 'id', false); - $this->assertEquals(['123', '345'], $result); - - $result = ArrayHelper::getColumn($array, function ($element) { - return $element['data']; - }); - $this->assertEquals(['a' => 'abc', 'b' => 'def'], $result); - $result = ArrayHelper::getColumn($array, function ($element) { - return $element['data']; - }, false); - $this->assertEquals(['abc', 'def'], $result); - } - - public function testMap() - { - $array = [ - ['id' => '123', 'name' => 'aaa', 'class' => 'x'], - ['id' => '124', 'name' => 'bbb', 'class' => 'x'], - ['id' => '345', 'name' => 'ccc', 'class' => 'y'], - ]; - - $result = ArrayHelper::map($array, 'id', 'name'); - $this->assertEquals([ - '123' => 'aaa', - '124' => 'bbb', - '345' => 'ccc', - ], $result); - - $result = ArrayHelper::map($array, 'id', 'name', 'class'); - $this->assertEquals([ - 'x' => [ - '123' => 'aaa', - '124' => 'bbb', - ], - 'y' => [ - '345' => 'ccc', - ], - ], $result); - } - - public function testKeyExists() - { - $array = [ - 'a' => 1, - 'B' => 2, - ]; - $this->assertTrue(ArrayHelper::keyExists('a', $array)); - $this->assertFalse(ArrayHelper::keyExists('b', $array)); - $this->assertTrue(ArrayHelper::keyExists('B', $array)); - $this->assertFalse(ArrayHelper::keyExists('c', $array)); - - $this->assertTrue(ArrayHelper::keyExists('a', $array, false)); - $this->assertTrue(ArrayHelper::keyExists('b', $array, false)); - $this->assertTrue(ArrayHelper::keyExists('B', $array, false)); - $this->assertFalse(ArrayHelper::keyExists('c', $array, false)); - } - - public function valueProvider() - { - return [ - ['name', 'test'], - ['noname', null], - ['noname', 'test', 'test'], - ['post.id', 5], - ['post.id', 5, 'test'], - ['nopost.id', null], - ['nopost.id', 'test', 'test'], - ['post.author.name', 'cebe'], - ['post.author.noname', null], - ['post.author.noname', 'test', 'test'], - ['post.author.profile.title', '1337'], - ['admin.firstname', 'Qiang'], - ['admin.firstname', 'Qiang', 'test'], - ['admin.lastname', 'Xue'], - [ - function ($array, $defaultValue) { - return $array['date'] . $defaultValue; - }, - '31-12-2113test', - 'test' - ], - ]; - } - - /** - * @dataProvider valueProvider - * - * @param $key - * @param $expected - * @param null $default - */ - public function testGetValue($key, $expected, $default = null) - { - $array = [ - 'name' => 'test', - 'date' => '31-12-2113', - 'post' => [ - 'id' => 5, - 'author' => [ - 'name' => 'cebe', - 'profile' => [ - 'title' => '1337', - ], - ], - ], - 'admin.firstname' => 'Qiang', - 'admin.lastname' => 'Xue', - 'admin' => [ - 'lastname' => 'cebe', - ], - ]; - - $this->assertEquals($expected, ArrayHelper::getValue($array, $key, $default)); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testToArray() + { + $object = new Post1; + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); + $object = new Post2; + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); + + $object1 = new Post1; + $object2 = new Post2; + $this->assertEquals([ + get_object_vars($object1), + get_object_vars($object2), + ], ArrayHelper::toArray([ + $object1, + $object2, + ])); + + $object = new Post2; + $this->assertEquals([ + 'id' => 123, + 'secret' => 's', + '_content' => 'test', + 'length' => 4, + ], ArrayHelper::toArray($object, [ + $object->className() => [ + 'id', 'secret', + '_content' => 'content', + 'length' => function ($post) { + return strlen($post->content); + } + ] + ])); + + $object = new Post3(); + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object, [], false)); + $this->assertEquals([ + 'id' => 33, + 'subObject' => [ + 'id' => 123, + 'content' => 'test', + ], + ], ArrayHelper::toArray($object)); + } + + public function testRemove() + { + $array = ['name' => 'b', 'age' => 3]; + $name = ArrayHelper::remove($array, 'name'); + + $this->assertEquals($name, 'b'); + $this->assertEquals($array, ['age' => 3]); + + $default = ArrayHelper::remove($array, 'nonExisting', 'defaultValue'); + $this->assertEquals('defaultValue', $default); + } + + public function testMultisort() + { + // single key + $array = [ + ['name' => 'b', 'age' => 3], + ['name' => 'a', 'age' => 1], + ['name' => 'c', 'age' => 2], + ]; + ArrayHelper::multisort($array, 'name'); + $this->assertEquals(['name' => 'a', 'age' => 1], $array[0]); + $this->assertEquals(['name' => 'b', 'age' => 3], $array[1]); + $this->assertEquals(['name' => 'c', 'age' => 2], $array[2]); + + // multiple keys + $array = [ + ['name' => 'b', 'age' => 3], + ['name' => 'a', 'age' => 2], + ['name' => 'a', 'age' => 1], + ]; + ArrayHelper::multisort($array, ['name', 'age']); + $this->assertEquals(['name' => 'a', 'age' => 1], $array[0]); + $this->assertEquals(['name' => 'a', 'age' => 2], $array[1]); + $this->assertEquals(['name' => 'b', 'age' => 3], $array[2]); + + // case-insensitive + $array = [ + ['name' => 'a', 'age' => 3], + ['name' => 'b', 'age' => 2], + ['name' => 'B', 'age' => 4], + ['name' => 'A', 'age' => 1], + ]; + + ArrayHelper::multisort($array, ['name', 'age'], SORT_ASC, [SORT_STRING, SORT_REGULAR]); + $this->assertEquals(['name' => 'A', 'age' => 1], $array[0]); + $this->assertEquals(['name' => 'B', 'age' => 4], $array[1]); + $this->assertEquals(['name' => 'a', 'age' => 3], $array[2]); + $this->assertEquals(['name' => 'b', 'age' => 2], $array[3]); + + ArrayHelper::multisort($array, ['name', 'age'], SORT_ASC, [SORT_STRING | SORT_FLAG_CASE, SORT_REGULAR]); + $this->assertEquals(['name' => 'A', 'age' => 1], $array[0]); + $this->assertEquals(['name' => 'a', 'age' => 3], $array[1]); + $this->assertEquals(['name' => 'b', 'age' => 2], $array[2]); + $this->assertEquals(['name' => 'B', 'age' => 4], $array[3]); + } + + public function testMultisortUseSort() + { + // single key + $sort = new Sort([ + 'attributes' => ['name', 'age'], + 'defaultOrder' => ['name' => SORT_ASC], + ]); + $orders = $sort->getOrders(); + + $array = [ + ['name' => 'b', 'age' => 3], + ['name' => 'a', 'age' => 1], + ['name' => 'c', 'age' => 2], + ]; + ArrayHelper::multisort($array, array_keys($orders), array_values($orders)); + $this->assertEquals(['name' => 'a', 'age' => 1], $array[0]); + $this->assertEquals(['name' => 'b', 'age' => 3], $array[1]); + $this->assertEquals(['name' => 'c', 'age' => 2], $array[2]); + + // multiple keys + $sort = new Sort([ + 'attributes' => ['name', 'age'], + 'defaultOrder' => ['name' => SORT_ASC, 'age' => SORT_DESC], + ]); + $orders = $sort->getOrders(); + + $array = [ + ['name' => 'b', 'age' => 3], + ['name' => 'a', 'age' => 2], + ['name' => 'a', 'age' => 1], + ]; + ArrayHelper::multisort($array, array_keys($orders), array_values($orders)); + $this->assertEquals(['name' => 'a', 'age' => 2], $array[0]); + $this->assertEquals(['name' => 'a', 'age' => 1], $array[1]); + $this->assertEquals(['name' => 'b', 'age' => 3], $array[2]); + } + + public function testMerge() + { + $a = [ + 'name' => 'Yii', + 'version' => '1.0', + 'options' => [ + 'namespace' => false, + 'unittest' => false, + ], + 'features' => [ + 'mvc', + ], + ]; + $b = [ + 'version' => '1.1', + 'options' => [ + 'unittest' => true, + ], + 'features' => [ + 'gii', + ], + ]; + $c = [ + 'version' => '2.0', + 'options' => [ + 'namespace' => true, + ], + 'features' => [ + 'debug', + ], + ]; + + $result = ArrayHelper::merge($a, $b, $c); + $expected = [ + 'name' => 'Yii', + 'version' => '2.0', + 'options' => [ + 'namespace' => true, + 'unittest' => true, + ], + 'features' => [ + 'mvc', + 'gii', + 'debug', + ], + ]; + + $this->assertEquals($expected, $result); + } + + public function testIndex() + { + $array = [ + ['id' => '123', 'data' => 'abc'], + ['id' => '345', 'data' => 'def'], + ]; + $result = ArrayHelper::index($array, 'id'); + $this->assertEquals([ + '123' => ['id' => '123', 'data' => 'abc'], + '345' => ['id' => '345', 'data' => 'def'], + ], $result); + + $result = ArrayHelper::index($array, function ($element) { + return $element['data']; + }); + $this->assertEquals([ + 'abc' => ['id' => '123', 'data' => 'abc'], + 'def' => ['id' => '345', 'data' => 'def'], + ], $result); + } + + public function testGetColumn() + { + $array = [ + 'a' => ['id' => '123', 'data' => 'abc'], + 'b' => ['id' => '345', 'data' => 'def'], + ]; + $result = ArrayHelper::getColumn($array, 'id'); + $this->assertEquals(['a' => '123', 'b' => '345'], $result); + $result = ArrayHelper::getColumn($array, 'id', false); + $this->assertEquals(['123', '345'], $result); + + $result = ArrayHelper::getColumn($array, function ($element) { + return $element['data']; + }); + $this->assertEquals(['a' => 'abc', 'b' => 'def'], $result); + $result = ArrayHelper::getColumn($array, function ($element) { + return $element['data']; + }, false); + $this->assertEquals(['abc', 'def'], $result); + } + + public function testMap() + { + $array = [ + ['id' => '123', 'name' => 'aaa', 'class' => 'x'], + ['id' => '124', 'name' => 'bbb', 'class' => 'x'], + ['id' => '345', 'name' => 'ccc', 'class' => 'y'], + ]; + + $result = ArrayHelper::map($array, 'id', 'name'); + $this->assertEquals([ + '123' => 'aaa', + '124' => 'bbb', + '345' => 'ccc', + ], $result); + + $result = ArrayHelper::map($array, 'id', 'name', 'class'); + $this->assertEquals([ + 'x' => [ + '123' => 'aaa', + '124' => 'bbb', + ], + 'y' => [ + '345' => 'ccc', + ], + ], $result); + } + + public function testKeyExists() + { + $array = [ + 'a' => 1, + 'B' => 2, + ]; + $this->assertTrue(ArrayHelper::keyExists('a', $array)); + $this->assertFalse(ArrayHelper::keyExists('b', $array)); + $this->assertTrue(ArrayHelper::keyExists('B', $array)); + $this->assertFalse(ArrayHelper::keyExists('c', $array)); + + $this->assertTrue(ArrayHelper::keyExists('a', $array, false)); + $this->assertTrue(ArrayHelper::keyExists('b', $array, false)); + $this->assertTrue(ArrayHelper::keyExists('B', $array, false)); + $this->assertFalse(ArrayHelper::keyExists('c', $array, false)); + } + + public function valueProvider() + { + return [ + ['name', 'test'], + ['noname', null], + ['noname', 'test', 'test'], + ['post.id', 5], + ['post.id', 5, 'test'], + ['nopost.id', null], + ['nopost.id', 'test', 'test'], + ['post.author.name', 'cebe'], + ['post.author.noname', null], + ['post.author.noname', 'test', 'test'], + ['post.author.profile.title', '1337'], + ['admin.firstname', 'Qiang'], + ['admin.firstname', 'Qiang', 'test'], + ['admin.lastname', 'Xue'], + [ + function ($array, $defaultValue) { + return $array['date'] . $defaultValue; + }, + '31-12-2113test', + 'test' + ], + ]; + } + + /** + * @dataProvider valueProvider + * + * @param $key + * @param $expected + * @param null $default + */ + public function testGetValue($key, $expected, $default = null) + { + $array = [ + 'name' => 'test', + 'date' => '31-12-2113', + 'post' => [ + 'id' => 5, + 'author' => [ + 'name' => 'cebe', + 'profile' => [ + 'title' => '1337', + ], + ], + ], + 'admin.firstname' => 'Qiang', + 'admin.lastname' => 'Xue', + 'admin' => [ + 'lastname' => 'cebe', + ], + ]; + + $this->assertEquals($expected, ArrayHelper::getValue($array, $key, $default)); + } } diff --git a/tests/unit/framework/helpers/ConsoleTest.php b/tests/unit/framework/helpers/ConsoleTest.php index ce6d3fe02f0..8f4e5c85722 100644 --- a/tests/unit/framework/helpers/ConsoleTest.php +++ b/tests/unit/framework/helpers/ConsoleTest.php @@ -12,71 +12,71 @@ */ class ConsoleTest extends TestCase { - public function testStripAnsiFormat() - { - ob_start(); - ob_implicit_flush(false); - echo 'a'; - Console::moveCursorForward(1); - echo 'a'; - Console::moveCursorDown(1); - echo 'a'; - Console::moveCursorUp(1); - echo 'a'; - Console::moveCursorBackward(1); - echo 'a'; - Console::moveCursorNextLine(1); - echo 'a'; - Console::moveCursorPrevLine(1); - echo 'a'; - Console::moveCursorTo(1); - echo 'a'; - Console::moveCursorTo(1, 2); - echo 'a'; - Console::clearLine(); - echo 'a'; - Console::clearLineAfterCursor(); - echo 'a'; - Console::clearLineBeforeCursor(); - echo 'a'; - Console::clearScreen(); - echo 'a'; - Console::clearScreenAfterCursor(); - echo 'a'; - Console::clearScreenBeforeCursor(); - echo 'a'; - Console::scrollDown(); - echo 'a'; - Console::scrollUp(); - echo 'a'; - Console::hideCursor(); - echo 'a'; - Console::showCursor(); - echo 'a'; - Console::saveCursorPosition(); - echo 'a'; - Console::restoreCursorPosition(); - echo 'a'; - Console::beginAnsiFormat([Console::FG_GREEN, Console::BG_BLUE, Console::UNDERLINE]); - echo 'a'; - Console::endAnsiFormat(); - echo 'a'; - Console::beginAnsiFormat([Console::xtermBgColor(128), Console::xtermFgColor(55)]); - echo 'a'; - Console::endAnsiFormat(); - echo 'a'; - $ouput = Console::stripAnsiFormat(ob_get_clean()); - ob_implicit_flush(true); - // $output = str_replace("\033", 'X003', $ouput );// uncomment for debugging - $this->assertEquals(str_repeat('a', 25), $ouput); - } + public function testStripAnsiFormat() + { + ob_start(); + ob_implicit_flush(false); + echo 'a'; + Console::moveCursorForward(1); + echo 'a'; + Console::moveCursorDown(1); + echo 'a'; + Console::moveCursorUp(1); + echo 'a'; + Console::moveCursorBackward(1); + echo 'a'; + Console::moveCursorNextLine(1); + echo 'a'; + Console::moveCursorPrevLine(1); + echo 'a'; + Console::moveCursorTo(1); + echo 'a'; + Console::moveCursorTo(1, 2); + echo 'a'; + Console::clearLine(); + echo 'a'; + Console::clearLineAfterCursor(); + echo 'a'; + Console::clearLineBeforeCursor(); + echo 'a'; + Console::clearScreen(); + echo 'a'; + Console::clearScreenAfterCursor(); + echo 'a'; + Console::clearScreenBeforeCursor(); + echo 'a'; + Console::scrollDown(); + echo 'a'; + Console::scrollUp(); + echo 'a'; + Console::hideCursor(); + echo 'a'; + Console::showCursor(); + echo 'a'; + Console::saveCursorPosition(); + echo 'a'; + Console::restoreCursorPosition(); + echo 'a'; + Console::beginAnsiFormat([Console::FG_GREEN, Console::BG_BLUE, Console::UNDERLINE]); + echo 'a'; + Console::endAnsiFormat(); + echo 'a'; + Console::beginAnsiFormat([Console::xtermBgColor(128), Console::xtermFgColor(55)]); + echo 'a'; + Console::endAnsiFormat(); + echo 'a'; + $ouput = Console::stripAnsiFormat(ob_get_clean()); + ob_implicit_flush(true); + // $output = str_replace("\033", 'X003', $ouput );// uncomment for debugging + $this->assertEquals(str_repeat('a', 25), $ouput); + } /* public function testScreenSize() - { - for ($i = 1; $i < 20; $i++) { - echo implode(', ', Console::getScreenSize(true)) . "\n"; - ob_flush(); - sleep(1); - } - }*/ + { + for ($i = 1; $i < 20; $i++) { + echo implode(', ', Console::getScreenSize(true)) . "\n"; + ob_flush(); + sleep(1); + } + }*/ } diff --git a/tests/unit/framework/helpers/FileHelperTest.php b/tests/unit/framework/helpers/FileHelperTest.php index ade18f05a65..0c1a9bcc52c 100644 --- a/tests/unit/framework/helpers/FileHelperTest.php +++ b/tests/unit/framework/helpers/FileHelperTest.php @@ -10,377 +10,377 @@ */ class FileHelperTest extends TestCase { - /** - * @var string test files path. - */ - private $testFilePath = ''; - - public function setUp() - { - $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); - $this->createDir($this->testFilePath); - if (!file_exists($this->testFilePath)) { - $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); - } - } - - public function tearDown() - { - $this->removeDir($this->testFilePath); - } - - /** - * Creates directory. - * @param string $dirName directory full name. - */ - protected function createDir($dirName) - { - if (!file_exists($dirName)) { - mkdir($dirName, 0777, true); - } - } - - /** - * Removes directory. - * @param string $dirName directory full name. - */ - protected function removeDir($dirName) - { - if (!empty($dirName) && is_dir($dirName)) { - if ($handle = opendir($dirName)) { - while (false !== ($entry = readdir($handle))) { - if ($entry != '.' && $entry != '..') { - if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) { - $this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry); - } else { - unlink($dirName . DIRECTORY_SEPARATOR . $entry); - } - } - } - closedir($handle); - rmdir($dirName); - } - } - } - - /** - * Get file permission mode. - * @param string $file file name. - * @return string permission mode. - */ - protected function getMode($file) - { - return substr(sprintf('%o', fileperms($file)), -4); - } - - /** - * Creates test files structure, - * @param array $items file system objects to be created in format: objectName => objectContent - * Arrays specifies directories, other values - files. - * @param string $basePath structure base file path. - */ - protected function createFileStructure(array $items, $basePath = '') - { - if (empty($basePath)) { - $basePath = $this->testFilePath; - } - foreach ($items as $name => $content) { - $itemName = $basePath . DIRECTORY_SEPARATOR . $name; - if (is_array($content)) { - mkdir($itemName, 0777, true); - $this->createFileStructure($content, $itemName); - } else { - file_put_contents($itemName, $content); - } - } - } - - /** - * Asserts that file has specific permission mode. - * @param integer $expectedMode expected file permission mode. - * @param string $fileName file name. - * @param string $message error message - */ - protected function assertFileMode($expectedMode, $fileName, $message = '') - { - $expectedMode = sprintf('%o', $expectedMode); - $this->assertEquals($expectedMode, $this->getMode($fileName), $message); - } - - // Tests : - - public function testCopyDirectory() - { - $srcDirName = 'test_src_dir'; - $files = [ - 'file1.txt' => 'file 1 content', - 'file2.txt' => 'file 2 content', - ]; - $this->createFileStructure([ - $srcDirName => $files - ]); - - $basePath = $this->testFilePath; - $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; - $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; - - FileHelper::copyDirectory($srcDirName, $dstDirName); - - $this->assertFileExists($dstDirName, 'Destination directory does not exist!'); - foreach ($files as $name => $content) { - $fileName = $dstDirName . DIRECTORY_SEPARATOR . $name; - $this->assertFileExists($fileName); - $this->assertEquals($content, file_get_contents($fileName), 'Incorrect file content!'); - } - } - - /** - * @depends testCopyDirectory - */ - public function testCopyDirectoryPermissions() - { - if (substr(PHP_OS, 0, 3) == 'WIN') { - $this->markTestSkipped("Can't reliably test it on Windows because fileperms() always return 0777."); - } - - $srcDirName = 'test_src_dir'; - $subDirName = 'test_sub_dir'; - $fileName = 'test_file.txt'; - $this->createFileStructure([ - $srcDirName => [ - $subDirName => [], - $fileName => 'test file content', - ], - ]); - - $basePath = $this->testFilePath; - $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; - $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; - - $dirMode = 0755; - $fileMode = 0755; - $options = [ - 'dirMode' => $dirMode, - 'fileMode' => $fileMode, - ]; - FileHelper::copyDirectory($srcDirName, $dstDirName, $options); - - $this->assertFileMode($dirMode, $dstDirName, 'Destination directory has wrong mode!'); - $this->assertFileMode($dirMode, $dstDirName . DIRECTORY_SEPARATOR . $subDirName, 'Copied sub directory has wrong mode!'); - $this->assertFileMode($fileMode, $dstDirName . DIRECTORY_SEPARATOR . $fileName, 'Copied file has wrong mode!'); - } - - public function testRemoveDirectory() - { - $dirName = 'test_dir_for_remove'; - $this->createFileStructure([ - $dirName => [ - 'file1.txt' => 'file 1 content', - 'file2.txt' => 'file 2 content', - 'test_sub_dir' => [ - 'sub_dir_file_1.txt' => 'sub dir file 1 content', - 'sub_dir_file_2.txt' => 'sub dir file 2 content', - ], - ], - ]); - - $basePath = $this->testFilePath; - $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; - - FileHelper::removeDirectory($dirName); - - $this->assertFileNotExists($dirName, 'Unable to remove directory!'); - - // should be silent about non-existing directories - FileHelper::removeDirectory($basePath . DIRECTORY_SEPARATOR . 'nonExisting'); - } - - public function testFindFiles() - { - $dirName = 'test_dir'; - $this->createFileStructure([ - $dirName => [ - 'file_1.txt' => 'file 1 content', - 'file_2.txt' => 'file 2 content', - 'test_sub_dir' => [ - 'file_1_1.txt' => 'sub dir file 1 content', - 'file_1_2.txt' => 'sub dir file 2 content', - ], - ], - ]); - $basePath = $this->testFilePath; - $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; - $expectedFiles = [ - $dirName . DIRECTORY_SEPARATOR . 'file_1.txt', - $dirName . DIRECTORY_SEPARATOR . 'file_2.txt', - $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_1.txt', - $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_2.txt', - ]; - - $foundFiles = FileHelper::findFiles($dirName); - sort($expectedFiles); - sort($foundFiles); - $this->assertEquals($expectedFiles, $foundFiles); - } - - /** - * @depends testFindFiles - */ - public function testFindFileFilter() - { - $dirName = 'test_dir'; - $passedFileName = 'passed.txt'; - $this->createFileStructure([ - $dirName => [ - $passedFileName => 'passed file content', - 'declined.txt' => 'declined file content', - ], - ]); - $basePath = $this->testFilePath; - $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; - - $options = [ - 'filter' => function ($path) use ($passedFileName) { - return $passedFileName == basename($path); - } - ]; - $foundFiles = FileHelper::findFiles($dirName, $options); - $this->assertEquals([$dirName . DIRECTORY_SEPARATOR . $passedFileName], $foundFiles); - } - - /** - * @depends testFindFiles - */ - public function testFindFilesExclude() - { - $basePath = $this->testFilePath . DIRECTORY_SEPARATOR; - $dirs = ['', 'one', 'one' . DIRECTORY_SEPARATOR . 'two', 'three']; - $files = array_fill_keys(array_map(function ($n) { - return "a.$n"; - }, range(1, 8)), 'file contents'); - - $tree = $files; - $root = $files; - $flat = []; - foreach ($dirs as $dir) { - foreach ($files as $fileName => $contents) { - $flat[] = rtrim($basePath . $dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName; - } - if ($dir === '') { - continue; - } - $parts = explode(DIRECTORY_SEPARATOR, $dir); - $last = array_pop($parts); - $parent = array_pop($parts); - $tree[$last] = $files; - if ($parent !== null) { - $tree[$parent][$last] = &$tree[$last]; - } else { - $root[$last] = &$tree[$last]; - } - } - $this->createFileStructure($root); - - // range - $foundFiles = FileHelper::findFiles($basePath, ['except' => ['a.[2-8]']]); - sort($foundFiles); - $expect = array_values(array_filter($flat, function ($p) { - return substr($p, -3)==='a.1'; - })); - $this->assertEquals($expect, $foundFiles); - - // suffix - $foundFiles = FileHelper::findFiles($basePath, ['except' => ['*.1']]); - sort($foundFiles); - $expect = array_values(array_filter($flat, function ($p) { - return substr($p, -3)!=='a.1'; - })); - $this->assertEquals($expect, $foundFiles); - - // dir - $foundFiles = FileHelper::findFiles($basePath, ['except' => ['/one']]); - sort($foundFiles); - $expect = array_values(array_filter($flat, function ($p) { - return strpos($p, DIRECTORY_SEPARATOR.'one')===false; - })); - $this->assertEquals($expect, $foundFiles); - - // dir contents - $foundFiles = FileHelper::findFiles($basePath, ['except' => ['?*/a.1']]); - sort($foundFiles); - $expect = array_values(array_filter($flat, function ($p) { - return substr($p, -11, 10)==='one'.DIRECTORY_SEPARATOR.'two'.DIRECTORY_SEPARATOR.'a.' || ( - substr($p, -8)!==DIRECTORY_SEPARATOR.'one'.DIRECTORY_SEPARATOR.'a.1' && - substr($p, -10)!==DIRECTORY_SEPARATOR.'three'.DIRECTORY_SEPARATOR.'a.1' - ); - })); - $this->assertEquals($expect, $foundFiles); - } - - public function testCreateDirectory() - { - $basePath = $this->testFilePath; - $dirName = $basePath . DIRECTORY_SEPARATOR . 'test_dir_level_1' . DIRECTORY_SEPARATOR . 'test_dir_level_2'; - $this->assertTrue(FileHelper::createDirectory($dirName), 'FileHelper::createDirectory should return true if directory was created!'); - $this->assertFileExists($dirName, 'Unable to create directory recursively!'); - $this->assertTrue(FileHelper::createDirectory($dirName), 'FileHelper::createDirectory should return true for already existing directories!'); - } - - public function testGetMimeTypeByExtension() - { - $magicFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type.php'; - $mimeTypeMap = [ - 'txa' => 'application/json', - 'txb' => 'another/mime', - ]; - $magicFileContent = ' $mimeType) { - $fileName = 'test.' . $extension; - $this->assertNull(FileHelper::getMimeTypeByExtension($fileName)); - $this->assertEquals($mimeType, FileHelper::getMimeTypeByExtension($fileName, $magicFile)); - } - } - - public function testGetMimeType() - { - $file = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type_test.txt'; - file_put_contents($file, 'some text'); - $this->assertEquals('text/plain', FileHelper::getMimeType($file)); - - $file = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type_test.json'; - file_put_contents($file, '{"a": "b"}'); - $this->assertEquals('text/plain', FileHelper::getMimeType($file)); - } - - public function testNormalizePath() - { - $this->assertEquals(DIRECTORY_SEPARATOR.'home'.DIRECTORY_SEPARATOR.'demo', FileHelper::normalizePath('/home\demo/')); - } - - public function testLocalizedDirectory() - { - $this->createFileStructure([ - 'views' => [ - 'faq.php' => 'English FAQ', - 'de-DE' => [ - 'faq.php' => 'German FAQ', - ], - ], - ]); - $viewFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'faq.php'; - $sourceLanguage = 'en-US'; - - // Source language and target language are same. The view path should be unchanged. - $currentLanguage = $sourceLanguage; - $this->assertSame($viewFile, FileHelper::localize($viewFile, $currentLanguage, $sourceLanguage)); - - // Source language and target language are different. The view path should be changed. - $currentLanguage = 'de-DE'; - $this->assertSame( - $this->testFilePath . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $currentLanguage . DIRECTORY_SEPARATOR . 'faq.php', - FileHelper::localize($viewFile, $currentLanguage, $sourceLanguage) - ); - } + /** + * @var string test files path. + */ + private $testFilePath = ''; + + public function setUp() + { + $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); + $this->createDir($this->testFilePath); + if (!file_exists($this->testFilePath)) { + $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); + } + } + + public function tearDown() + { + $this->removeDir($this->testFilePath); + } + + /** + * Creates directory. + * @param string $dirName directory full name. + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param string $dirName directory full name. + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && is_dir($dirName)) { + if ($handle = opendir($dirName)) { + while (false !== ($entry = readdir($handle))) { + if ($entry != '.' && $entry != '..') { + if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) { + $this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry); + } else { + unlink($dirName . DIRECTORY_SEPARATOR . $entry); + } + } + } + closedir($handle); + rmdir($dirName); + } + } + } + + /** + * Get file permission mode. + * @param string $file file name. + * @return string permission mode. + */ + protected function getMode($file) + { + return substr(sprintf('%o', fileperms($file)), -4); + } + + /** + * Creates test files structure, + * @param array $items file system objects to be created in format: objectName => objectContent + * Arrays specifies directories, other values - files. + * @param string $basePath structure base file path. + */ + protected function createFileStructure(array $items, $basePath = '') + { + if (empty($basePath)) { + $basePath = $this->testFilePath; + } + foreach ($items as $name => $content) { + $itemName = $basePath . DIRECTORY_SEPARATOR . $name; + if (is_array($content)) { + mkdir($itemName, 0777, true); + $this->createFileStructure($content, $itemName); + } else { + file_put_contents($itemName, $content); + } + } + } + + /** + * Asserts that file has specific permission mode. + * @param integer $expectedMode expected file permission mode. + * @param string $fileName file name. + * @param string $message error message + */ + protected function assertFileMode($expectedMode, $fileName, $message = '') + { + $expectedMode = sprintf('%o', $expectedMode); + $this->assertEquals($expectedMode, $this->getMode($fileName), $message); + } + + // Tests : + + public function testCopyDirectory() + { + $srcDirName = 'test_src_dir'; + $files = [ + 'file1.txt' => 'file 1 content', + 'file2.txt' => 'file 2 content', + ]; + $this->createFileStructure([ + $srcDirName => $files + ]); + + $basePath = $this->testFilePath; + $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; + $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; + + FileHelper::copyDirectory($srcDirName, $dstDirName); + + $this->assertFileExists($dstDirName, 'Destination directory does not exist!'); + foreach ($files as $name => $content) { + $fileName = $dstDirName . DIRECTORY_SEPARATOR . $name; + $this->assertFileExists($fileName); + $this->assertEquals($content, file_get_contents($fileName), 'Incorrect file content!'); + } + } + + /** + * @depends testCopyDirectory + */ + public function testCopyDirectoryPermissions() + { + if (substr(PHP_OS, 0, 3) == 'WIN') { + $this->markTestSkipped("Can't reliably test it on Windows because fileperms() always return 0777."); + } + + $srcDirName = 'test_src_dir'; + $subDirName = 'test_sub_dir'; + $fileName = 'test_file.txt'; + $this->createFileStructure([ + $srcDirName => [ + $subDirName => [], + $fileName => 'test file content', + ], + ]); + + $basePath = $this->testFilePath; + $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; + $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; + + $dirMode = 0755; + $fileMode = 0755; + $options = [ + 'dirMode' => $dirMode, + 'fileMode' => $fileMode, + ]; + FileHelper::copyDirectory($srcDirName, $dstDirName, $options); + + $this->assertFileMode($dirMode, $dstDirName, 'Destination directory has wrong mode!'); + $this->assertFileMode($dirMode, $dstDirName . DIRECTORY_SEPARATOR . $subDirName, 'Copied sub directory has wrong mode!'); + $this->assertFileMode($fileMode, $dstDirName . DIRECTORY_SEPARATOR . $fileName, 'Copied file has wrong mode!'); + } + + public function testRemoveDirectory() + { + $dirName = 'test_dir_for_remove'; + $this->createFileStructure([ + $dirName => [ + 'file1.txt' => 'file 1 content', + 'file2.txt' => 'file 2 content', + 'test_sub_dir' => [ + 'sub_dir_file_1.txt' => 'sub dir file 1 content', + 'sub_dir_file_2.txt' => 'sub dir file 2 content', + ], + ], + ]); + + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + FileHelper::removeDirectory($dirName); + + $this->assertFileNotExists($dirName, 'Unable to remove directory!'); + + // should be silent about non-existing directories + FileHelper::removeDirectory($basePath . DIRECTORY_SEPARATOR . 'nonExisting'); + } + + public function testFindFiles() + { + $dirName = 'test_dir'; + $this->createFileStructure([ + $dirName => [ + 'file_1.txt' => 'file 1 content', + 'file_2.txt' => 'file 2 content', + 'test_sub_dir' => [ + 'file_1_1.txt' => 'sub dir file 1 content', + 'file_1_2.txt' => 'sub dir file 2 content', + ], + ], + ]); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + $expectedFiles = [ + $dirName . DIRECTORY_SEPARATOR . 'file_1.txt', + $dirName . DIRECTORY_SEPARATOR . 'file_2.txt', + $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_1.txt', + $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_2.txt', + ]; + + $foundFiles = FileHelper::findFiles($dirName); + sort($expectedFiles); + sort($foundFiles); + $this->assertEquals($expectedFiles, $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFileFilter() + { + $dirName = 'test_dir'; + $passedFileName = 'passed.txt'; + $this->createFileStructure([ + $dirName => [ + $passedFileName => 'passed file content', + 'declined.txt' => 'declined file content', + ], + ]); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + $options = [ + 'filter' => function ($path) use ($passedFileName) { + return $passedFileName == basename($path); + } + ]; + $foundFiles = FileHelper::findFiles($dirName, $options); + $this->assertEquals([$dirName . DIRECTORY_SEPARATOR . $passedFileName], $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFilesExclude() + { + $basePath = $this->testFilePath . DIRECTORY_SEPARATOR; + $dirs = ['', 'one', 'one' . DIRECTORY_SEPARATOR . 'two', 'three']; + $files = array_fill_keys(array_map(function ($n) { + return "a.$n"; + }, range(1, 8)), 'file contents'); + + $tree = $files; + $root = $files; + $flat = []; + foreach ($dirs as $dir) { + foreach ($files as $fileName => $contents) { + $flat[] = rtrim($basePath . $dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName; + } + if ($dir === '') { + continue; + } + $parts = explode(DIRECTORY_SEPARATOR, $dir); + $last = array_pop($parts); + $parent = array_pop($parts); + $tree[$last] = $files; + if ($parent !== null) { + $tree[$parent][$last] = &$tree[$last]; + } else { + $root[$last] = &$tree[$last]; + } + } + $this->createFileStructure($root); + + // range + $foundFiles = FileHelper::findFiles($basePath, ['except' => ['a.[2-8]']]); + sort($foundFiles); + $expect = array_values(array_filter($flat, function ($p) { + return substr($p, -3)==='a.1'; + })); + $this->assertEquals($expect, $foundFiles); + + // suffix + $foundFiles = FileHelper::findFiles($basePath, ['except' => ['*.1']]); + sort($foundFiles); + $expect = array_values(array_filter($flat, function ($p) { + return substr($p, -3)!=='a.1'; + })); + $this->assertEquals($expect, $foundFiles); + + // dir + $foundFiles = FileHelper::findFiles($basePath, ['except' => ['/one']]); + sort($foundFiles); + $expect = array_values(array_filter($flat, function ($p) { + return strpos($p, DIRECTORY_SEPARATOR.'one')===false; + })); + $this->assertEquals($expect, $foundFiles); + + // dir contents + $foundFiles = FileHelper::findFiles($basePath, ['except' => ['?*/a.1']]); + sort($foundFiles); + $expect = array_values(array_filter($flat, function ($p) { + return substr($p, -11, 10)==='one'.DIRECTORY_SEPARATOR.'two'.DIRECTORY_SEPARATOR.'a.' || ( + substr($p, -8)!==DIRECTORY_SEPARATOR.'one'.DIRECTORY_SEPARATOR.'a.1' && + substr($p, -10)!==DIRECTORY_SEPARATOR.'three'.DIRECTORY_SEPARATOR.'a.1' + ); + })); + $this->assertEquals($expect, $foundFiles); + } + + public function testCreateDirectory() + { + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . 'test_dir_level_1' . DIRECTORY_SEPARATOR . 'test_dir_level_2'; + $this->assertTrue(FileHelper::createDirectory($dirName), 'FileHelper::createDirectory should return true if directory was created!'); + $this->assertFileExists($dirName, 'Unable to create directory recursively!'); + $this->assertTrue(FileHelper::createDirectory($dirName), 'FileHelper::createDirectory should return true for already existing directories!'); + } + + public function testGetMimeTypeByExtension() + { + $magicFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type.php'; + $mimeTypeMap = [ + 'txa' => 'application/json', + 'txb' => 'another/mime', + ]; + $magicFileContent = ' $mimeType) { + $fileName = 'test.' . $extension; + $this->assertNull(FileHelper::getMimeTypeByExtension($fileName)); + $this->assertEquals($mimeType, FileHelper::getMimeTypeByExtension($fileName, $magicFile)); + } + } + + public function testGetMimeType() + { + $file = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type_test.txt'; + file_put_contents($file, 'some text'); + $this->assertEquals('text/plain', FileHelper::getMimeType($file)); + + $file = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type_test.json'; + file_put_contents($file, '{"a": "b"}'); + $this->assertEquals('text/plain', FileHelper::getMimeType($file)); + } + + public function testNormalizePath() + { + $this->assertEquals(DIRECTORY_SEPARATOR.'home'.DIRECTORY_SEPARATOR.'demo', FileHelper::normalizePath('/home\demo/')); + } + + public function testLocalizedDirectory() + { + $this->createFileStructure([ + 'views' => [ + 'faq.php' => 'English FAQ', + 'de-DE' => [ + 'faq.php' => 'German FAQ', + ], + ], + ]); + $viewFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'faq.php'; + $sourceLanguage = 'en-US'; + + // Source language and target language are same. The view path should be unchanged. + $currentLanguage = $sourceLanguage; + $this->assertSame($viewFile, FileHelper::localize($viewFile, $currentLanguage, $sourceLanguage)); + + // Source language and target language are different. The view path should be changed. + $currentLanguage = 'de-DE'; + $this->assertSame( + $this->testFilePath . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $currentLanguage . DIRECTORY_SEPARATOR . 'faq.php', + FileHelper::localize($viewFile, $currentLanguage, $sourceLanguage) + ); + } } diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 7578444f687..ada3a9e4ad3 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -11,464 +11,464 @@ */ class HtmlTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication([ - 'components' => [ - 'request' => [ - 'class' => 'yii\web\Request', - 'url' => '/test', - 'enableCsrfValidation' => false, - ], - 'response' => [ - 'class' => 'yii\web\Response', - ], - ], - ]); - } - - public function assertEqualsWithoutLE($expected, $actual) - { - $expected = str_replace("\r\n", "\n", $expected); - $actual = str_replace("\r\n", "\n", $actual); - - $this->assertEquals($expected, $actual); - } - - public function testEncode() - { - $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); - } - - public function testDecode() - { - $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); - } - - public function testTag() - { - $this->assertEquals('
        ', Html::tag('br')); - $this->assertEquals('', Html::tag('span')); - $this->assertEquals('
        content
        ', Html::tag('div', 'content')); - $this->assertEquals('', Html::tag('input', '', ['type' => 'text', 'name' => 'test', 'value' => '<>'])); - $this->assertEquals('', Html::tag('span', '', ['disabled' => true])); - } - - public function testBeginTag() - { - $this->assertEquals('
        ', Html::beginTag('br')); - $this->assertEquals('', Html::beginTag('span', ['id' => 'test', 'class' => 'title'])); - } - - public function testEndTag() - { - $this->assertEquals('
        ', Html::endTag('br')); - $this->assertEquals('
        ', Html::endTag('span')); - } - - public function testStyle() - { - $content = 'a <>'; - $this->assertEquals("", Html::style($content)); - $this->assertEquals("", Html::style($content, ['type' => 'text/less'])); - } - - public function testScript() - { - $content = 'a <>'; - $this->assertEquals("", Html::script($content)); - $this->assertEquals("", Html::script($content, ['type' => 'text/js'])); - } - - public function testCssFile() - { - $this->assertEquals('', Html::cssFile('http://example.com')); - $this->assertEquals('', Html::cssFile('')); - } - - public function testJsFile() - { - $this->assertEquals('', Html::jsFile('http://example.com')); - $this->assertEquals('', Html::jsFile('')); - } - - public function testBeginForm() - { - $this->assertEquals('
        ', Html::beginForm()); - $this->assertEquals('', Html::beginForm('/example', 'get')); - $hiddens = [ - '', - '', - ]; - $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); - } - - public function testEndForm() - { - $this->assertEquals('
        ', Html::endForm()); - } - - public function testA() - { - $this->assertEquals('something<>', Html::a('something<>')); - $this->assertEquals('something', Html::a('something', '/example')); - $this->assertEquals('something', Html::a('something', '')); - } - - public function testMailto() - { - $this->assertEquals('test<>', Html::mailto('test<>')); - $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); - } - - public function testImg() - { - $this->assertEquals('', Html::img('/example')); - $this->assertEquals('', Html::img('')); - $this->assertEquals('something', Html::img('/example', ['alt' => 'something', 'width' => 10])); - } - - public function testLabel() - { - $this->assertEquals('', Html::label('something<>')); - $this->assertEquals('', Html::label('something<>', 'a')); - $this->assertEquals('', Html::label('something<>', 'a', ['class' => 'test'])); - } - - public function testButton() - { - $this->assertEquals('', Html::button()); - $this->assertEquals('', Html::button('content<>', ['name' => 'test', 'value' => 'value'])); - $this->assertEquals('', Html::button('content<>', ['type' => 'submit', 'name' => 'test', 'value' => 'value', 'class' => "t"])); - } - - public function testSubmitButton() - { - $this->assertEquals('', Html::submitButton()); - $this->assertEquals('', Html::submitButton('content<>', ['name' => 'test', 'value' => 'value', 'class' => 't'])); - } - - public function testResetButton() - { - $this->assertEquals('', Html::resetButton()); - $this->assertEquals('', Html::resetButton('content<>', ['name' => 'test', 'value' => 'value', 'class' => 't'])); - } - - public function testInput() - { - $this->assertEquals('', Html::input('text')); - $this->assertEquals('', Html::input('text', 'test', 'value', ['class' => 't'])); - } - - public function testButtonInput() - { - $this->assertEquals('', Html::buttonInput()); - $this->assertEquals('', Html::buttonInput('text', ['name' => 'test', 'class' => 'a'])); - } - - public function testSubmitInput() - { - $this->assertEquals('', Html::submitInput()); - $this->assertEquals('', Html::submitInput('text', ['name' => 'test', 'class' => 'a'])); - } - - public function testResetInput() - { - $this->assertEquals('', Html::resetInput()); - $this->assertEquals('', Html::resetInput('text', ['name' => 'test', 'class' => 'a'])); - } - - public function testTextInput() - { - $this->assertEquals('', Html::textInput('test')); - $this->assertEquals('', Html::textInput('test', 'value', ['class' => 't'])); - } - - public function testHiddenInput() - { - $this->assertEquals('', Html::hiddenInput('test')); - $this->assertEquals('', Html::hiddenInput('test', 'value', ['class' => 't'])); - } - - public function testPasswordInput() - { - $this->assertEquals('', Html::passwordInput('test')); - $this->assertEquals('', Html::passwordInput('test', 'value', ['class' => 't'])); - } - - public function testFileInput() - { - $this->assertEquals('', Html::fileInput('test')); - $this->assertEquals('', Html::fileInput('test', 'value', ['class' => 't'])); - } - - public function testTextarea() - { - $this->assertEquals('', Html::textarea('test')); - $this->assertEquals('', Html::textarea('test', 'value<>', ['class' => 't'])); - } - - public function testRadio() - { - $this->assertEquals('', Html::radio('test')); - $this->assertEquals('', Html::radio('test', true, ['class' => 'a', 'value' => null])); - $this->assertEquals('', Html::radio('test', true, ['class' => 'a', 'uncheck' => '0', 'value' => 2])); - - $this->assertEquals('
        ', Html::radio('test', true, [ - 'class' => 'a', - 'value' => null, - 'label' => 'ccc', - 'labelOptions' => ['class' =>'bbb'], - ])); - $this->assertEquals('
        ', Html::radio('test', true, [ - 'class' => 'a', - 'uncheck' => '0', - 'label' => 'ccc', - 'value' => 2, - ])); - } - - public function testCheckbox() - { - $this->assertEquals('', Html::checkbox('test')); - $this->assertEquals('', Html::checkbox('test', true, ['class' => 'a', 'value' => null])); - $this->assertEquals('', Html::checkbox('test', true, ['class' => 'a', 'uncheck' => '0', 'value' => 2])); - - $this->assertEquals('
        ', Html::checkbox('test', true, [ - 'class' => 'a', - 'value' => null, - 'label' => 'ccc', - 'labelOptions' => ['class' =>'bbb'], - ])); - $this->assertEquals('
        ', Html::checkbox('test', true, [ - 'class' => 'a', - 'uncheck' => '0', - 'label' => 'ccc', - 'value' => 2, - ])); - } - - public function testDropDownList() - { - $expected = <<mockApplication([ + 'components' => [ + 'request' => [ + 'class' => 'yii\web\Request', + 'url' => '/test', + 'enableCsrfValidation' => false, + ], + 'response' => [ + 'class' => 'yii\web\Response', + ], + ], + ]); + } + + public function assertEqualsWithoutLE($expected, $actual) + { + $expected = str_replace("\r\n", "\n", $expected); + $actual = str_replace("\r\n", "\n", $actual); + + $this->assertEquals($expected, $actual); + } + + public function testEncode() + { + $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); + } + + public function testDecode() + { + $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); + } + + public function testTag() + { + $this->assertEquals('
        ', Html::tag('br')); + $this->assertEquals('', Html::tag('span')); + $this->assertEquals('
        content
        ', Html::tag('div', 'content')); + $this->assertEquals('', Html::tag('input', '', ['type' => 'text', 'name' => 'test', 'value' => '<>'])); + $this->assertEquals('', Html::tag('span', '', ['disabled' => true])); + } + + public function testBeginTag() + { + $this->assertEquals('
        ', Html::beginTag('br')); + $this->assertEquals('', Html::beginTag('span', ['id' => 'test', 'class' => 'title'])); + } + + public function testEndTag() + { + $this->assertEquals('
        ', Html::endTag('br')); + $this->assertEquals('
        ', Html::endTag('span')); + } + + public function testStyle() + { + $content = 'a <>'; + $this->assertEquals("", Html::style($content)); + $this->assertEquals("", Html::style($content, ['type' => 'text/less'])); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("", Html::script($content)); + $this->assertEquals("", Html::script($content, ['type' => 'text/js'])); + } + + public function testCssFile() + { + $this->assertEquals('', Html::cssFile('http://example.com')); + $this->assertEquals('', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('', Html::jsFile('http://example.com')); + $this->assertEquals('', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('
        ', Html::beginForm()); + $this->assertEquals('', Html::beginForm('/example', 'get')); + $hiddens = [ + '', + '', + ]; + $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('
        ', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('something<>', Html::a('something<>')); + $this->assertEquals('something', Html::a('something', '/example')); + $this->assertEquals('something', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('test<>', Html::mailto('test<>')); + $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('', Html::img('/example')); + $this->assertEquals('', Html::img('')); + $this->assertEquals('something', Html::img('/example', ['alt' => 'something', 'width' => 10])); + } + + public function testLabel() + { + $this->assertEquals('', Html::label('something<>')); + $this->assertEquals('', Html::label('something<>', 'a')); + $this->assertEquals('', Html::label('something<>', 'a', ['class' => 'test'])); + } + + public function testButton() + { + $this->assertEquals('', Html::button()); + $this->assertEquals('', Html::button('content<>', ['name' => 'test', 'value' => 'value'])); + $this->assertEquals('', Html::button('content<>', ['type' => 'submit', 'name' => 'test', 'value' => 'value', 'class' => "t"])); + } + + public function testSubmitButton() + { + $this->assertEquals('', Html::submitButton()); + $this->assertEquals('', Html::submitButton('content<>', ['name' => 'test', 'value' => 'value', 'class' => 't'])); + } + + public function testResetButton() + { + $this->assertEquals('', Html::resetButton()); + $this->assertEquals('', Html::resetButton('content<>', ['name' => 'test', 'value' => 'value', 'class' => 't'])); + } + + public function testInput() + { + $this->assertEquals('', Html::input('text')); + $this->assertEquals('', Html::input('text', 'test', 'value', ['class' => 't'])); + } + + public function testButtonInput() + { + $this->assertEquals('', Html::buttonInput()); + $this->assertEquals('', Html::buttonInput('text', ['name' => 'test', 'class' => 'a'])); + } + + public function testSubmitInput() + { + $this->assertEquals('', Html::submitInput()); + $this->assertEquals('', Html::submitInput('text', ['name' => 'test', 'class' => 'a'])); + } + + public function testResetInput() + { + $this->assertEquals('', Html::resetInput()); + $this->assertEquals('', Html::resetInput('text', ['name' => 'test', 'class' => 'a'])); + } + + public function testTextInput() + { + $this->assertEquals('', Html::textInput('test')); + $this->assertEquals('', Html::textInput('test', 'value', ['class' => 't'])); + } + + public function testHiddenInput() + { + $this->assertEquals('', Html::hiddenInput('test')); + $this->assertEquals('', Html::hiddenInput('test', 'value', ['class' => 't'])); + } + + public function testPasswordInput() + { + $this->assertEquals('', Html::passwordInput('test')); + $this->assertEquals('', Html::passwordInput('test', 'value', ['class' => 't'])); + } + + public function testFileInput() + { + $this->assertEquals('', Html::fileInput('test')); + $this->assertEquals('', Html::fileInput('test', 'value', ['class' => 't'])); + } + + public function testTextarea() + { + $this->assertEquals('', Html::textarea('test')); + $this->assertEquals('', Html::textarea('test', 'value<>', ['class' => 't'])); + } + + public function testRadio() + { + $this->assertEquals('', Html::radio('test')); + $this->assertEquals('', Html::radio('test', true, ['class' => 'a', 'value' => null])); + $this->assertEquals('', Html::radio('test', true, ['class' => 'a', 'uncheck' => '0', 'value' => 2])); + + $this->assertEquals('
        ', Html::radio('test', true, [ + 'class' => 'a', + 'value' => null, + 'label' => 'ccc', + 'labelOptions' => ['class' =>'bbb'], + ])); + $this->assertEquals('
        ', Html::radio('test', true, [ + 'class' => 'a', + 'uncheck' => '0', + 'label' => 'ccc', + 'value' => 2, + ])); + } + + public function testCheckbox() + { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', true, ['class' => 'a', 'value' => null])); + $this->assertEquals('', Html::checkbox('test', true, ['class' => 'a', 'uncheck' => '0', 'value' => 2])); + + $this->assertEquals('
        ', Html::checkbox('test', true, [ + 'class' => 'a', + 'value' => null, + 'label' => 'ccc', + 'labelOptions' => ['class' =>'bbb'], + ])); + $this->assertEquals('
        ', Html::checkbox('test', true, [ + 'class' => 'a', + 'uncheck' => '0', + 'label' => 'ccc', + 'value' => 2, + ])); + } + + public function testDropDownList() + { + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::dropDownList('test')); - $expected = <<assertEqualsWithoutLE($expected, Html::dropDownList('test')); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems())); - $expected = <<assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); - } + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } - public function testListBox() - { - $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test')); - $expected = <<assertEqualsWithoutLE($expected, Html::listBox('test')); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), ['size' => 5])); - $expected = <<assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), ['size' => 5])); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2())); - $expected = <<assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); - $expected = <<assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test', ['value1', 'value2'], $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', ['value1', 'value2'], $this->getDataItems())); - $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, [], ['multiple' => true])); - $expected = <<assertEqualsWithoutLE($expected, Html::listBox('test', null, [], ['multiple' => true])); + $expected = << EOD; - $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', [], ['unselect' => '0'])); - } + $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', [], ['unselect' => '0'])); + } - public function testCheckboxList() - { - $this->assertEquals('
        ', Html::checkboxList('test')); + public function testCheckboxList() + { + $this->assertEquals('
        ', Html::checkboxList('test')); - $expected = <<
        EOD; - $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems())); - $expected = <<
        EOD; - $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems2())); - $expected = <<

        EOD; - $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems(), [ - 'separator' => "
        \n", - 'unselect' => '0', - ])); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems(), [ + 'separator' => "
        \n", + 'unselect' => '0', + ])); - $expected = <<0 1 EOD; - $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems(), [ - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, ['value' => $value])); - } - ])); - } - - public function testRadioList() - { - $this->assertEquals('
        ', Html::radioList('test')); - - $expected = <<assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems(), [ + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, ['value' => $value])); + } + ])); + } + + public function testRadioList() + { + $this->assertEquals('
        ', Html::radioList('test')); + + $expected = <<
        EOD; - $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems())); - $expected = <<
        EOD; - $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems2())); - $expected = <<

        EOD; - $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems(), [ - 'separator' => "
        \n", - 'unselect' => '0', - ])); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems(), [ + 'separator' => "
        \n", + 'unselect' => '0', + ])); - $expected = <<0 1 EOD; - $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems(), [ - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::radio($name, $checked, ['value' => $value])); - } - ])); - } - - public function testUl() - { - $data = [ - 1, 'abc', '<>', - ]; - $expected = <<assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $this->getDataItems(), [ + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, ['value' => $value])); + } + ])); + } + + public function testUl() + { + $data = [ + 1, 'abc', '<>', + ]; + $expected = <<
      • 1
      • abc
      • <>
      • EOD; - $this->assertEqualsWithoutLE($expected, Html::ul($data)); - $expected = <<assertEqualsWithoutLE($expected, Html::ul($data)); + $expected = <<
      • 1
      • abc
      • <>
      • EOD; - $this->assertEqualsWithoutLE($expected, Html::ul($data, [ - 'class' => 'test', - 'item' => function ($item, $index) { - return "
      • $item
      • "; - } - ])); - } - - public function testOl() - { - $data = [ - 1, 'abc', '<>', - ]; - $expected = <<assertEqualsWithoutLE($expected, Html::ul($data, [ + 'class' => 'test', + 'item' => function ($item, $index) { + return "
      • $item
      • "; + } + ])); + } + + public function testOl() + { + $data = [ + 1, 'abc', '<>', + ]; + $expected = <<
      • 1
      • abc
      • <>
      • EOD; - $this->assertEqualsWithoutLE($expected, Html::ol($data, [ - 'itemOptions' => ['class' => 'ti'], - ])); - $expected = <<assertEqualsWithoutLE($expected, Html::ol($data, [ + 'itemOptions' => ['class' => 'ti'], + ])); + $expected = <<
      • 1
      • abc
      • <>
      • EOD; - $this->assertEqualsWithoutLE($expected, Html::ol($data, [ - 'class' => 'test', - 'item' => function ($item, $index) { - return "
      • $item
      • "; - } - ])); - } - - public function testRenderOptions() - { - $data = [ - 'value1' => 'label1', - 'group1' => [ - 'value11' => 'label11', - 'group11' => [ - 'value111' => 'label111', - ], - 'group12' => [], - ], - 'value2' => 'label2', - 'group2' => [], - ]; - $expected = <<assertEqualsWithoutLE($expected, Html::ol($data, [ + 'class' => 'test', + 'item' => function ($item, $index) { + return "
      • $item
      • "; + } + ])); + } + + public function testRenderOptions() + { + $data = [ + 'value1' => 'label1', + 'group1' => [ + 'value11' => 'label11', + 'group11' => [ + 'value111' => 'label111', + ], + 'group12' => [], + ], + 'value2' => 'label2', + 'group2' => [], + ]; + $expected = <<please select<> @@ -485,133 +485,133 @@ public function testRenderOptions() EOD; - $attributes = [ - 'prompt' => 'please select<>', - 'options' => [ - 'value111' => ['class' => 'option'], - ], - 'groups' => [ - 'group12' => ['class' => 'group'], - ], - ]; - $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(['value111', 'value1'], $data, $attributes)); - } - - public function testRenderAttributes() - { - $this->assertEquals('', Html::renderTagAttributes([])); - $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(['name' => 'test', 'empty' => null, 'value' => '1<>'])); - $this->assertEquals(' checked disabled', Html::renderTagAttributes(['checked' => true, 'disabled' => true, 'hidden' => false])); - } - - public function testAddCssClass() - { - $options = []; - Html::addCssClass($options, 'test'); - $this->assertEquals(['class' => 'test'], $options); - Html::addCssClass($options, 'test'); - $this->assertEquals(['class' => 'test'], $options); - Html::addCssClass($options, 'test2'); - $this->assertEquals(['class' => 'test test2'], $options); - Html::addCssClass($options, 'test'); - $this->assertEquals(['class' => 'test test2'], $options); - Html::addCssClass($options, 'test2'); - $this->assertEquals(['class' => 'test test2'], $options); - Html::addCssClass($options, 'test3'); - $this->assertEquals(['class' => 'test test2 test3'], $options); - Html::addCssClass($options, 'test2'); - $this->assertEquals(['class' => 'test test2 test3'], $options); - } - - public function testRemoveCssClass() - { - $options = ['class' => 'test test2 test3']; - Html::removeCssClass($options, 'test2'); - $this->assertEquals(['class' => 'test test3'], $options); - Html::removeCssClass($options, 'test2'); - $this->assertEquals(['class' => 'test test3'], $options); - Html::removeCssClass($options, 'test'); - $this->assertEquals(['class' => 'test3'], $options); - Html::removeCssClass($options, 'test3'); - $this->assertEquals([], $options); - } - - public function testCssStyleFromArray() - { - $this->assertEquals('width: 100px; height: 200px;', Html::cssStyleFromArray([ - 'width' => '100px', - 'height' => '200px', - ])); - $this->assertNull(Html::cssStyleFromArray([])); - } - - public function testCssStyleToArray() - { - $this->assertEquals([ - 'width' => '100px', - 'height' => '200px', - ], Html::cssStyleToArray('width: 100px; height: 200px;')); - $this->assertEquals([], Html::cssStyleToArray(' ')); - } - - public function testAddCssStyle() - { - $options = ['style' => 'width: 100px; height: 200px;']; - Html::addCssStyle($options, 'width: 110px; color: red;'); - $this->assertEquals('width: 110px; height: 200px; color: red;', $options['style']); - - $options = ['style' => 'width: 100px; height: 200px;']; - Html::addCssStyle($options, ['width' => '110px', 'color' => 'red']); - $this->assertEquals('width: 110px; height: 200px; color: red;', $options['style']); - - $options = ['style' => 'width: 100px; height: 200px;']; - Html::addCssStyle($options, 'width: 110px; color: red;', false); - $this->assertEquals('width: 100px; height: 200px; color: red;', $options['style']); - - $options = []; - Html::addCssStyle($options, 'width: 110px; color: red;'); - $this->assertEquals('width: 110px; color: red;', $options['style']); - - $options = []; - Html::addCssStyle($options, 'width: 110px; color: red;', false); - $this->assertEquals('width: 110px; color: red;', $options['style']); - } - - public function testRemoveCssStyle() - { - $options = ['style' => 'width: 110px; height: 200px; color: red;']; - Html::removeCssStyle($options, 'width'); - $this->assertEquals('height: 200px; color: red;', $options['style']); - Html::removeCssStyle($options, ['height']); - $this->assertEquals('color: red;', $options['style']); - Html::removeCssStyle($options, ['color', 'background']); - $this->assertNull($options['style']); - - $options = []; - Html::removeCssStyle($options, ['color', 'background']); - $this->assertTrue(!array_key_exists('style', $options)); - } - - public function testBooleanAttributes() - { - $this->assertEquals('', Html::input('email', 'mail', null, ['required' => false])); - $this->assertEquals('', Html::input('email', 'mail', null, ['required' => true])); - $this->assertEquals('', Html::input('email', 'mail', null, ['required' => 'hi'])); - } - - protected function getDataItems() - { - return [ - 'value1' => 'text1', - 'value2' => 'text2', - ]; - } - - protected function getDataItems2() - { - return [ - 'value1<>' => 'text1<>', - 'value 2' => 'text 2', - ]; - } + $attributes = [ + 'prompt' => 'please select<>', + 'options' => [ + 'value111' => ['class' => 'option'], + ], + 'groups' => [ + 'group12' => ['class' => 'group'], + ], + ]; + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(['value111', 'value1'], $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes([])); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(['name' => 'test', 'empty' => null, 'value' => '1<>'])); + $this->assertEquals(' checked disabled', Html::renderTagAttributes(['checked' => true, 'disabled' => true, 'hidden' => false])); + } + + public function testAddCssClass() + { + $options = []; + Html::addCssClass($options, 'test'); + $this->assertEquals(['class' => 'test'], $options); + Html::addCssClass($options, 'test'); + $this->assertEquals(['class' => 'test'], $options); + Html::addCssClass($options, 'test2'); + $this->assertEquals(['class' => 'test test2'], $options); + Html::addCssClass($options, 'test'); + $this->assertEquals(['class' => 'test test2'], $options); + Html::addCssClass($options, 'test2'); + $this->assertEquals(['class' => 'test test2'], $options); + Html::addCssClass($options, 'test3'); + $this->assertEquals(['class' => 'test test2 test3'], $options); + Html::addCssClass($options, 'test2'); + $this->assertEquals(['class' => 'test test2 test3'], $options); + } + + public function testRemoveCssClass() + { + $options = ['class' => 'test test2 test3']; + Html::removeCssClass($options, 'test2'); + $this->assertEquals(['class' => 'test test3'], $options); + Html::removeCssClass($options, 'test2'); + $this->assertEquals(['class' => 'test test3'], $options); + Html::removeCssClass($options, 'test'); + $this->assertEquals(['class' => 'test3'], $options); + Html::removeCssClass($options, 'test3'); + $this->assertEquals([], $options); + } + + public function testCssStyleFromArray() + { + $this->assertEquals('width: 100px; height: 200px;', Html::cssStyleFromArray([ + 'width' => '100px', + 'height' => '200px', + ])); + $this->assertNull(Html::cssStyleFromArray([])); + } + + public function testCssStyleToArray() + { + $this->assertEquals([ + 'width' => '100px', + 'height' => '200px', + ], Html::cssStyleToArray('width: 100px; height: 200px;')); + $this->assertEquals([], Html::cssStyleToArray(' ')); + } + + public function testAddCssStyle() + { + $options = ['style' => 'width: 100px; height: 200px;']; + Html::addCssStyle($options, 'width: 110px; color: red;'); + $this->assertEquals('width: 110px; height: 200px; color: red;', $options['style']); + + $options = ['style' => 'width: 100px; height: 200px;']; + Html::addCssStyle($options, ['width' => '110px', 'color' => 'red']); + $this->assertEquals('width: 110px; height: 200px; color: red;', $options['style']); + + $options = ['style' => 'width: 100px; height: 200px;']; + Html::addCssStyle($options, 'width: 110px; color: red;', false); + $this->assertEquals('width: 100px; height: 200px; color: red;', $options['style']); + + $options = []; + Html::addCssStyle($options, 'width: 110px; color: red;'); + $this->assertEquals('width: 110px; color: red;', $options['style']); + + $options = []; + Html::addCssStyle($options, 'width: 110px; color: red;', false); + $this->assertEquals('width: 110px; color: red;', $options['style']); + } + + public function testRemoveCssStyle() + { + $options = ['style' => 'width: 110px; height: 200px; color: red;']; + Html::removeCssStyle($options, 'width'); + $this->assertEquals('height: 200px; color: red;', $options['style']); + Html::removeCssStyle($options, ['height']); + $this->assertEquals('color: red;', $options['style']); + Html::removeCssStyle($options, ['color', 'background']); + $this->assertNull($options['style']); + + $options = []; + Html::removeCssStyle($options, ['color', 'background']); + $this->assertTrue(!array_key_exists('style', $options)); + } + + public function testBooleanAttributes() + { + $this->assertEquals('', Html::input('email', 'mail', null, ['required' => false])); + $this->assertEquals('', Html::input('email', 'mail', null, ['required' => true])); + $this->assertEquals('', Html::input('email', 'mail', null, ['required' => 'hi'])); + } + + protected function getDataItems() + { + return [ + 'value1' => 'text1', + 'value2' => 'text2', + ]; + } + + protected function getDataItems2() + { + return [ + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ]; + } } diff --git a/tests/unit/framework/helpers/InflectorTest.php b/tests/unit/framework/helpers/InflectorTest.php index ff414ba082f..9d1c44ad0c7 100644 --- a/tests/unit/framework/helpers/InflectorTest.php +++ b/tests/unit/framework/helpers/InflectorTest.php @@ -11,137 +11,137 @@ */ class InflectorTest extends TestCase { - public function testPluralize() - { - $testData = [ - 'move' => 'moves', - 'foot' => 'feet', - 'child' => 'children', - 'human' => 'humans', - 'man' => 'men', - 'staff' => 'staff', - 'tooth' => 'teeth', - 'person' => 'people', - 'mouse' => 'mice', - 'touch' => 'touches', - 'hash' => 'hashes', - 'shelf' => 'shelves', - 'potato' => 'potatoes', - 'bus' => 'buses', - 'test' => 'tests', - 'car' => 'cars', - ]; - - foreach ($testData as $testIn => $testOut) { - $this->assertEquals($testOut, Inflector::pluralize($testIn)); - $this->assertEquals(ucfirst($testOut), ucfirst(Inflector::pluralize($testIn))); - } - } - - public function testSingularize() - { - $testData = [ - 'moves' => 'move', - 'feet' => 'foot', - 'children' => 'child', - 'humans' => 'human', - 'men' => 'man', - 'staff' => 'staff', - 'teeth' => 'tooth', - 'people' => 'person', - 'mice' => 'mouse', - 'touches' => 'touch', - 'hashes' => 'hash', - 'shelves' => 'shelf', - 'potatoes' => 'potato', - 'buses' => 'bus', - 'tests' => 'test', - 'cars' => 'car', - ]; - foreach ($testData as $testIn => $testOut) { - $this->assertEquals($testOut, Inflector::singularize($testIn)); - $this->assertEquals(ucfirst($testOut), ucfirst(Inflector::singularize($testIn))); - } - } - - public function testTitleize() - { - $this->assertEquals("Me my self and i", Inflector::titleize('MeMySelfAndI')); - $this->assertEquals("Me My Self And I", Inflector::titleize('MeMySelfAndI', true)); - } - - public function testCamelize() - { - $this->assertEquals("MeMySelfAndI", Inflector::camelize('me my_self-andI')); - $this->assertEquals("QweQweEwq", Inflector::camelize('qwe qwe^ewq')); - } - - public function testUnderscore() - { - $this->assertEquals("me_my_self_and_i", Inflector::underscore('MeMySelfAndI')); - } - - public function testCamel2words() - { - $this->assertEquals('Camel Case', Inflector::camel2words('camelCase')); - $this->assertEquals('Lower Case', Inflector::camel2words('lower_case')); - $this->assertEquals('Tricky Stuff It Is Testing', Inflector::camel2words(' tricky_stuff.it-is testing... ')); - } - - public function testCamel2id() - { - $this->assertEquals('post-tag', Inflector::camel2id('PostTag')); - $this->assertEquals('post_tag', Inflector::camel2id('PostTag', '_')); - - $this->assertEquals('post-tag', Inflector::camel2id('postTag')); - $this->assertEquals('post_tag', Inflector::camel2id('postTag', '_')); - } - - public function testId2camel() - { - $this->assertEquals('PostTag', Inflector::id2camel('post-tag')); - $this->assertEquals('PostTag', Inflector::id2camel('post_tag', '_')); - - $this->assertEquals('PostTag', Inflector::id2camel('post-tag')); - $this->assertEquals('PostTag', Inflector::id2camel('post_tag', '_')); - } - - public function testHumanize() - { - $this->assertEquals("Me my self and i", Inflector::humanize('me_my_self_and_i')); - $this->assertEquals("Me My Self And I", Inflector::humanize('me_my_self_and_i', true)); - } - - public function testVariablize() - { - $this->assertEquals("customerTable", Inflector::variablize('customer_table')); - } - - public function testTableize() - { - $this->assertEquals("customer_tables", Inflector::tableize('customerTable')); - } - - public function testSlug() - { - $this->assertEquals("privet-hello-jii-framework-kak-dela-how-it-goes", Inflector::slug('Привет Hello Йии-- Framework !--- Как дела ? How it goes ?')); - - $this->assertEquals("this-is-a-title", Inflector::slug('this is a title')); - } - - public function testClassify() - { - $this->assertEquals("CustomerTable", Inflector::classify('customer_tables')); - } - - public function testOrdinalize() - { - $this->assertEquals('21st', Inflector::ordinalize('21')); - $this->assertEquals('22nd', Inflector::ordinalize('22')); - $this->assertEquals('23rd', Inflector::ordinalize('23')); - $this->assertEquals('24th', Inflector::ordinalize('24')); - $this->assertEquals('25th', Inflector::ordinalize('25')); - $this->assertEquals('111th', Inflector::ordinalize('111')); - $this->assertEquals('113th', Inflector::ordinalize('113')); - } + public function testPluralize() + { + $testData = [ + 'move' => 'moves', + 'foot' => 'feet', + 'child' => 'children', + 'human' => 'humans', + 'man' => 'men', + 'staff' => 'staff', + 'tooth' => 'teeth', + 'person' => 'people', + 'mouse' => 'mice', + 'touch' => 'touches', + 'hash' => 'hashes', + 'shelf' => 'shelves', + 'potato' => 'potatoes', + 'bus' => 'buses', + 'test' => 'tests', + 'car' => 'cars', + ]; + + foreach ($testData as $testIn => $testOut) { + $this->assertEquals($testOut, Inflector::pluralize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(Inflector::pluralize($testIn))); + } + } + + public function testSingularize() + { + $testData = [ + 'moves' => 'move', + 'feet' => 'foot', + 'children' => 'child', + 'humans' => 'human', + 'men' => 'man', + 'staff' => 'staff', + 'teeth' => 'tooth', + 'people' => 'person', + 'mice' => 'mouse', + 'touches' => 'touch', + 'hashes' => 'hash', + 'shelves' => 'shelf', + 'potatoes' => 'potato', + 'buses' => 'bus', + 'tests' => 'test', + 'cars' => 'car', + ]; + foreach ($testData as $testIn => $testOut) { + $this->assertEquals($testOut, Inflector::singularize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(Inflector::singularize($testIn))); + } + } + + public function testTitleize() + { + $this->assertEquals("Me my self and i", Inflector::titleize('MeMySelfAndI')); + $this->assertEquals("Me My Self And I", Inflector::titleize('MeMySelfAndI', true)); + } + + public function testCamelize() + { + $this->assertEquals("MeMySelfAndI", Inflector::camelize('me my_self-andI')); + $this->assertEquals("QweQweEwq", Inflector::camelize('qwe qwe^ewq')); + } + + public function testUnderscore() + { + $this->assertEquals("me_my_self_and_i", Inflector::underscore('MeMySelfAndI')); + } + + public function testCamel2words() + { + $this->assertEquals('Camel Case', Inflector::camel2words('camelCase')); + $this->assertEquals('Lower Case', Inflector::camel2words('lower_case')); + $this->assertEquals('Tricky Stuff It Is Testing', Inflector::camel2words(' tricky_stuff.it-is testing... ')); + } + + public function testCamel2id() + { + $this->assertEquals('post-tag', Inflector::camel2id('PostTag')); + $this->assertEquals('post_tag', Inflector::camel2id('PostTag', '_')); + + $this->assertEquals('post-tag', Inflector::camel2id('postTag')); + $this->assertEquals('post_tag', Inflector::camel2id('postTag', '_')); + } + + public function testId2camel() + { + $this->assertEquals('PostTag', Inflector::id2camel('post-tag')); + $this->assertEquals('PostTag', Inflector::id2camel('post_tag', '_')); + + $this->assertEquals('PostTag', Inflector::id2camel('post-tag')); + $this->assertEquals('PostTag', Inflector::id2camel('post_tag', '_')); + } + + public function testHumanize() + { + $this->assertEquals("Me my self and i", Inflector::humanize('me_my_self_and_i')); + $this->assertEquals("Me My Self And I", Inflector::humanize('me_my_self_and_i', true)); + } + + public function testVariablize() + { + $this->assertEquals("customerTable", Inflector::variablize('customer_table')); + } + + public function testTableize() + { + $this->assertEquals("customer_tables", Inflector::tableize('customerTable')); + } + + public function testSlug() + { + $this->assertEquals("privet-hello-jii-framework-kak-dela-how-it-goes", Inflector::slug('Привет Hello Йии-- Framework !--- Как дела ? How it goes ?')); + + $this->assertEquals("this-is-a-title", Inflector::slug('this is a title')); + } + + public function testClassify() + { + $this->assertEquals("CustomerTable", Inflector::classify('customer_tables')); + } + + public function testOrdinalize() + { + $this->assertEquals('21st', Inflector::ordinalize('21')); + $this->assertEquals('22nd', Inflector::ordinalize('22')); + $this->assertEquals('23rd', Inflector::ordinalize('23')); + $this->assertEquals('24th', Inflector::ordinalize('24')); + $this->assertEquals('25th', Inflector::ordinalize('25')); + $this->assertEquals('111th', Inflector::ordinalize('111')); + $this->assertEquals('113th', Inflector::ordinalize('113')); + } } diff --git a/tests/unit/framework/helpers/JsonTest.php b/tests/unit/framework/helpers/JsonTest.php index 20da3474835..ba21cf468b5 100644 --- a/tests/unit/framework/helpers/JsonTest.php +++ b/tests/unit/framework/helpers/JsonTest.php @@ -1,6 +1,5 @@ assertSame('"1"', Json::encode($data)); + public function testEncode() + { + // basic data encoding + $data = '1'; + $this->assertSame('"1"', Json::encode($data)); - // simple array encoding - $data = [1, 2]; - $this->assertSame('[1,2]', Json::encode($data)); - $data = ['a' => 1, 'b' => 2]; - $this->assertSame('{"a":1,"b":2}', Json::encode($data)); + // simple array encoding + $data = [1, 2]; + $this->assertSame('[1,2]', Json::encode($data)); + $data = ['a' => 1, 'b' => 2]; + $this->assertSame('{"a":1,"b":2}', Json::encode($data)); - // simple object encoding - $data = new \stdClass(); - $data->a = 1; - $data->b = 2; - $this->assertSame('{"a":1,"b":2}', Json::encode($data)); + // simple object encoding + $data = new \stdClass(); + $data->a = 1; + $data->b = 2; + $this->assertSame('{"a":1,"b":2}', Json::encode($data)); - // expression encoding - $expression = 'function () {}'; - $data = new JsExpression($expression); - $this->assertSame($expression, Json::encode($data)); + // expression encoding + $expression = 'function () {}'; + $data = new JsExpression($expression); + $this->assertSame($expression, Json::encode($data)); - // complex data - $expression1 = 'function (a) {}'; - $expression2 = 'function (b) {}'; - $data = [ - 'a' => [ - 1, new JsExpression($expression1) - ], - 'b' => new JsExpression($expression2), - ]; - $this->assertSame("{\"a\":[1,$expression1],\"b\":$expression2}", Json::encode($data)); + // complex data + $expression1 = 'function (a) {}'; + $expression2 = 'function (b) {}'; + $data = [ + 'a' => [ + 1, new JsExpression($expression1) + ], + 'b' => new JsExpression($expression2), + ]; + $this->assertSame("{\"a\":[1,$expression1],\"b\":$expression2}", Json::encode($data)); - // https://github.com/yiisoft/yii2/issues/957 - $data = (object)null; - $this->assertSame('{}', Json::encode($data)); - } + // https://github.com/yiisoft/yii2/issues/957 + $data = (object) null; + $this->assertSame('{}', Json::encode($data)); + } - public function testDecode() - { - // basic data decoding - $json = '"1"'; - $this->assertSame('1', Json::decode($json)); + public function testDecode() + { + // basic data decoding + $json = '"1"'; + $this->assertSame('1', Json::decode($json)); - // array decoding - $json = '{"a":1,"b":2}'; - $this->assertSame(['a' => 1, 'b' => 2], Json::decode($json)); + // array decoding + $json = '{"a":1,"b":2}'; + $this->assertSame(['a' => 1, 'b' => 2], Json::decode($json)); - // exception - $json = '{"a":1,"b":2'; - $this->setExpectedException('yii\base\InvalidParamException'); - Json::decode($json); - } + // exception + $json = '{"a":1,"b":2'; + $this->setExpectedException('yii\base\InvalidParamException'); + Json::decode($json); + } } diff --git a/tests/unit/framework/helpers/SecurityTest.php b/tests/unit/framework/helpers/SecurityTest.php index 6a1d2fd1982..e9bdcdfc3cc 100644 --- a/tests/unit/framework/helpers/SecurityTest.php +++ b/tests/unit/framework/helpers/SecurityTest.php @@ -12,32 +12,32 @@ class SecurityTest extends TestCase { - public function testPasswordHash() - { - $password = 'secret'; - $hash = Security::generatePasswordHash($password); - $this->assertTrue(Security::validatePassword($password, $hash)); - $this->assertFalse(Security::validatePassword('test', $hash)); - } + public function testPasswordHash() + { + $password = 'secret'; + $hash = Security::generatePasswordHash($password); + $this->assertTrue(Security::validatePassword($password, $hash)); + $this->assertFalse(Security::validatePassword('test', $hash)); + } - public function testHashData() - { - $data = 'known data'; - $key = 'secret'; - $hashedData = Security::hashData($data, $key); - $this->assertFalse($data === $hashedData); - $this->assertEquals($data, Security::validateData($hashedData, $key)); - $hashedData[strlen($hashedData) - 1] = 'A'; - $this->assertFalse(Security::validateData($hashedData, $key)); - } + public function testHashData() + { + $data = 'known data'; + $key = 'secret'; + $hashedData = Security::hashData($data, $key); + $this->assertFalse($data === $hashedData); + $this->assertEquals($data, Security::validateData($hashedData, $key)); + $hashedData[strlen($hashedData) - 1] = 'A'; + $this->assertFalse(Security::validateData($hashedData, $key)); + } - public function testEncrypt() - { - $data = 'known data'; - $key = 'secret'; - $encryptedData = Security::encrypt($data, $key); - $this->assertFalse($data === $encryptedData); - $decryptedData = Security::decrypt($encryptedData, $key); - $this->assertEquals($data, $decryptedData); - } + public function testEncrypt() + { + $data = 'known data'; + $key = 'secret'; + $encryptedData = Security::encrypt($data, $key); + $this->assertFalse($data === $encryptedData); + $decryptedData = Security::decrypt($encryptedData, $key); + $this->assertEquals($data, $decryptedData); + } } diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index a641001d136..bb4c735693e 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -10,59 +10,59 @@ */ class StringHelperTest extends TestCase { - public function testStrlen() - { - $this->assertEquals(4, StringHelper::byteLength('this')); - $this->assertEquals(6, StringHelper::byteLength('это')); - } + public function testStrlen() + { + $this->assertEquals(4, StringHelper::byteLength('this')); + $this->assertEquals(6, StringHelper::byteLength('это')); + } - public function testSubstr() - { - $this->assertEquals('th', StringHelper::byteSubstr('this', 0, 2)); - $this->assertEquals('э', StringHelper::byteSubstr('это', 0, 2)); - } + public function testSubstr() + { + $this->assertEquals('th', StringHelper::byteSubstr('this', 0, 2)); + $this->assertEquals('э', StringHelper::byteSubstr('это', 0, 2)); + } - public function testBasename() - { - $this->assertEquals('', StringHelper::basename('')); + public function testBasename() + { + $this->assertEquals('', StringHelper::basename('')); - $this->assertEquals('file', StringHelper::basename('file')); - $this->assertEquals('file.test', StringHelper::basename('file.test', '.test2')); - $this->assertEquals('file', StringHelper::basename('file.test', '.test')); + $this->assertEquals('file', StringHelper::basename('file')); + $this->assertEquals('file.test', StringHelper::basename('file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('file.test', '.test')); - $this->assertEquals('file', StringHelper::basename('/file')); - $this->assertEquals('file.test', StringHelper::basename('/file.test', '.test2')); - $this->assertEquals('file', StringHelper::basename('/file.test', '.test')); + $this->assertEquals('file', StringHelper::basename('/file')); + $this->assertEquals('file.test', StringHelper::basename('/file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('/file.test', '.test')); - $this->assertEquals('file', StringHelper::basename('/path/to/file')); - $this->assertEquals('file.test', StringHelper::basename('/path/to/file.test', '.test2')); - $this->assertEquals('file', StringHelper::basename('/path/to/file.test', '.test')); + $this->assertEquals('file', StringHelper::basename('/path/to/file')); + $this->assertEquals('file.test', StringHelper::basename('/path/to/file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('/path/to/file.test', '.test')); - $this->assertEquals('file', StringHelper::basename('\file')); - $this->assertEquals('file.test', StringHelper::basename('\file.test', '.test2')); - $this->assertEquals('file', StringHelper::basename('\file.test', '.test')); + $this->assertEquals('file', StringHelper::basename('\file')); + $this->assertEquals('file.test', StringHelper::basename('\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('\file.test', '.test')); - $this->assertEquals('file', StringHelper::basename('C:\file')); - $this->assertEquals('file.test', StringHelper::basename('C:\file.test', '.test2')); - $this->assertEquals('file', StringHelper::basename('C:\file.test', '.test')); + $this->assertEquals('file', StringHelper::basename('C:\file')); + $this->assertEquals('file.test', StringHelper::basename('C:\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('C:\file.test', '.test')); - $this->assertEquals('file', StringHelper::basename('C:\path\to\file')); - $this->assertEquals('file.test', StringHelper::basename('C:\path\to\file.test', '.test2')); - $this->assertEquals('file', StringHelper::basename('C:\path\to\file.test', '.test')); + $this->assertEquals('file', StringHelper::basename('C:\path\to\file')); + $this->assertEquals('file.test', StringHelper::basename('C:\path\to\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('C:\path\to\file.test', '.test')); - // mixed paths - $this->assertEquals('file.test', StringHelper::basename('/path\to/file.test')); - $this->assertEquals('file.test', StringHelper::basename('/path/to\file.test')); - $this->assertEquals('file.test', StringHelper::basename('\path/to\file.test')); + // mixed paths + $this->assertEquals('file.test', StringHelper::basename('/path\to/file.test')); + $this->assertEquals('file.test', StringHelper::basename('/path/to\file.test')); + $this->assertEquals('file.test', StringHelper::basename('\path/to\file.test')); - // \ and / in suffix - $this->assertEquals('file', StringHelper::basename('/path/to/filete/st', 'te/st')); - $this->assertEquals('st', StringHelper::basename('/path/to/filete/st', 'te\st')); - $this->assertEquals('file', StringHelper::basename('/path/to/filete\st', 'te\st')); - $this->assertEquals('st', StringHelper::basename('/path/to/filete\st', 'te/st')); + // \ and / in suffix + $this->assertEquals('file', StringHelper::basename('/path/to/filete/st', 'te/st')); + $this->assertEquals('st', StringHelper::basename('/path/to/filete/st', 'te\st')); + $this->assertEquals('file', StringHelper::basename('/path/to/filete\st', 'te\st')); + $this->assertEquals('st', StringHelper::basename('/path/to/filete\st', 'te/st')); - // http://www.php.net/manual/en/function.basename.php#72254 - $this->assertEquals('foo', StringHelper::basename('/bar/foo/')); - $this->assertEquals('foo', StringHelper::basename('\\bar\\foo\\')); - } + // http://www.php.net/manual/en/function.basename.php#72254 + $this->assertEquals('foo', StringHelper::basename('/bar/foo/')); + $this->assertEquals('foo', StringHelper::basename('\\bar\\foo\\')); + } } diff --git a/tests/unit/framework/helpers/UrlTest.php b/tests/unit/framework/helpers/UrlTest.php index 19b5aea72d7..bfb08a87580 100644 --- a/tests/unit/framework/helpers/UrlTest.php +++ b/tests/unit/framework/helpers/UrlTest.php @@ -12,158 +12,158 @@ */ class UrlTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication([ - 'components' => [ - 'request' => [ - 'class' => 'yii\web\Request', - 'scriptUrl' => '/base/index.php', - 'hostInfo' => 'http://example.com/', - 'url' => '/base/index.php&r=site/current&id=42' - ], - ], - ], '\yii\web\Application'); - } - - /** - * Mocks controller action with parameters - * - * @param string $controllerId - * @param string $actionId - * @param string $moduleID - * @param array $params - */ - protected function mockAction($controllerId, $actionId, $moduleID = null, $params = []) - { - \Yii::$app->controller = $controller = new Controller($controllerId, \Yii::$app); - $controller->actionParams = $params; - $controller->action = new Action($actionId, $controller); - - if ($moduleID !== null) { - $controller->module = new Module($moduleID); - } - } - - protected function removeMockedAction() - { - \Yii::$app->controller = null; - } - - public function testToRoute() - { - $this->mockAction('page', 'view', null, ['id' => 10]); - - // If the route is an empty string, the current route will be used; - $this->assertEquals('/base/index.php?r=page/view', Url::toRoute('')); - $this->assertEquals('http://example.com/base/index.php?r=page/view', Url::toRoute('', true)); - $this->assertEquals('https://example.com/base/index.php?r=page/view', Url::toRoute('', 'https')); - - // If the route contains no slashes at all, it is considered to be an action ID of the current controller and - // will be prepended with uniqueId; - $this->assertEquals('/base/index.php?r=page/edit', Url::toRoute('edit')); - $this->assertEquals('/base/index.php?r=page/edit&id=20', Url::toRoute(['edit', 'id' => 20])); - $this->assertEquals('http://example.com/base/index.php?r=page/edit&id=20', Url::toRoute(['edit', 'id' => 20], true)); - $this->assertEquals('https://example.com/base/index.php?r=page/edit&id=20', Url::toRoute(['edit', 'id' => 20], 'https')); - - // If the route has no leading slash, it is considered to be a route relative - // to the current module and will be prepended with the module's uniqueId. - $this->mockAction('default', 'index', 'stats'); - $this->assertEquals('/base/index.php?r=stats/user/view', Url::toRoute('user/view')); - $this->assertEquals('/base/index.php?r=stats/user/view&id=42', Url::toRoute(['user/view', 'id' => 42])); - $this->assertEquals('http://example.com/base/index.php?r=stats/user/view&id=42', Url::toRoute(['user/view', 'id' => 42], true)); - $this->assertEquals('https://example.com/base/index.php?r=stats/user/view&id=42', Url::toRoute(['user/view', 'id' => 42], 'https')); - - // In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. - $this->removeMockedAction(); - - $this->assertEquals('/base/index.php?r=site/view', Url::toRoute('site/view')); - $this->assertEquals('http://example.com/base/index.php?r=site/view', Url::toRoute('site/view', true)); - $this->assertEquals('https://example.com/base/index.php?r=site/view', Url::toRoute('site/view', 'https')); - $this->assertEquals('/base/index.php?r=site/view&id=37', Url::toRoute(['site/view', 'id' => 37])); - } - - public function testTo() - { - // is an array: the first array element is considered a route, while the rest of the name-value - // pairs are treated as the parameters to be used for URL creation using Url::toRoute. - $this->mockAction('page', 'view', null, ['id' => 10]); - $this->assertEquals('/base/index.php?r=page/edit&id=20', Url::to(['edit', 'id' => 20])); - $this->assertEquals('/base/index.php?r=page/edit', Url::to(['edit'])); - $this->assertEquals('/base/index.php?r=page/view', Url::to([''])); - - $this->assertEquals('http://example.com/base/index.php?r=page/edit&id=20', Url::to(['edit', 'id' => 20], true)); - $this->assertEquals('http://example.com/base/index.php?r=page/edit', Url::to(['edit'], true)); - $this->assertEquals('http://example.com/base/index.php?r=page/view', Url::to([''], true)); - - $this->assertEquals('https://example.com/base/index.php?r=page/edit&id=20', Url::to(['edit', 'id' => 20], 'https')); - $this->assertEquals('https://example.com/base/index.php?r=page/edit', Url::to(['edit'], 'https')); - $this->assertEquals('https://example.com/base/index.php?r=page/view', Url::to([''], 'https')); - - //In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. - $this->removeMockedAction(); - - $this->assertEquals('/base/index.php?r=edit&id=20', Url::to(['edit', 'id' => 20])); - $this->assertEquals('/base/index.php?r=edit', Url::to(['edit'])); - $this->assertEquals('/base/index.php?r=', Url::to([''])); - - $this->assertEquals('http://example.com/base/index.php?r=edit&id=20', Url::to(['edit', 'id' => 20], true)); - $this->assertEquals('http://example.com/base/index.php?r=edit', Url::to(['edit'], true)); - $this->assertEquals('http://example.com/base/index.php?r=', Url::to([''], true)); - - $this->assertEquals('https://example.com/base/index.php?r=edit&id=20', Url::to(['edit', 'id' => 20], 'https')); - $this->assertEquals('https://example.com/base/index.php?r=edit', Url::to(['edit'], 'https')); - $this->assertEquals('https://example.com/base/index.php?r=', Url::to([''], 'https')); - - // is an empty string: the currently requested URL will be returned; - $this->mockAction('page', 'view', null, ['id' => 10]); - $this->assertEquals('/base/index.php&r=site/current&id=42', Url::to('')); - $this->assertEquals('http://example.com/base/index.php&r=site/current&id=42', Url::to('', true)); - $this->assertEquals('https://example.com/base/index.php&r=site/current&id=42', Url::to('', 'https')); - $this->removeMockedAction(); - - // is a non-empty string: it will first be processed by [[Yii::getAlias()]]. If the result - // is an absolute URL, it will be returned either without any change or, if schema was specified, with schema - // replaced; Otherwise, the result will be prefixed with [[\yii\web\Request::baseUrl]] and returned. - \Yii::setAlias('@web1', 'http://test.example.com/test/me1'); - \Yii::setAlias('@web2', 'test/me2'); - \Yii::setAlias('@web3', ''); - \Yii::setAlias('@web4', '/test'); - \Yii::setAlias('@web5', '#test'); - - $this->assertEquals('http://test.example.com/test/me1', Url::to('@web1')); - $this->assertEquals('http://test.example.com/test/me1', Url::to('@web1', true)); - $this->assertEquals('https://test.example.com/test/me1', Url::to('@web1', 'https')); - - $this->assertEquals('/base/test/me2', Url::to('@web2')); - $this->assertEquals('http://example.com/base/test/me2', Url::to('@web2', true)); - $this->assertEquals('https://example.com/base/test/me2', Url::to('@web2', 'https')); - - $this->assertEquals('/base/', Url::to('@web3')); - $this->assertEquals('http://example.com/base/', Url::to('@web3', true)); - $this->assertEquals('https://example.com/base/', Url::to('@web3', 'https')); - - $this->assertEquals('/test', Url::to('@web4')); - $this->assertEquals('http://example.com/test', Url::to('@web4', true)); - $this->assertEquals('https://example.com/test', Url::to('@web4', 'https')); - - $this->assertEquals('#test', Url::to('@web5')); - $this->assertEquals('http://example.com#test', Url::to('@web5', true)); - $this->assertEquals('https://example.com#test', Url::to('@web5', 'https')); - } - - public function testHome() - { - $this->assertEquals('/base/index.php', Url::home()); - $this->assertEquals('http://example.com/base/index.php', Url::home(true)); - $this->assertEquals('https://example.com/base/index.php', Url::home('https')); - } - - public function testCanonical() - { - $this->mockAction('page', 'view', null, ['id' => 10]); - $this->assertEquals('http://example.com/base/index.php?r=page/view&id=10', Url::canonical()); - $this->removeMockedAction(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication([ + 'components' => [ + 'request' => [ + 'class' => 'yii\web\Request', + 'scriptUrl' => '/base/index.php', + 'hostInfo' => 'http://example.com/', + 'url' => '/base/index.php&r=site/current&id=42' + ], + ], + ], '\yii\web\Application'); + } + + /** + * Mocks controller action with parameters + * + * @param string $controllerId + * @param string $actionId + * @param string $moduleID + * @param array $params + */ + protected function mockAction($controllerId, $actionId, $moduleID = null, $params = []) + { + \Yii::$app->controller = $controller = new Controller($controllerId, \Yii::$app); + $controller->actionParams = $params; + $controller->action = new Action($actionId, $controller); + + if ($moduleID !== null) { + $controller->module = new Module($moduleID); + } + } + + protected function removeMockedAction() + { + \Yii::$app->controller = null; + } + + public function testToRoute() + { + $this->mockAction('page', 'view', null, ['id' => 10]); + + // If the route is an empty string, the current route will be used; + $this->assertEquals('/base/index.php?r=page/view', Url::toRoute('')); + $this->assertEquals('http://example.com/base/index.php?r=page/view', Url::toRoute('', true)); + $this->assertEquals('https://example.com/base/index.php?r=page/view', Url::toRoute('', 'https')); + + // If the route contains no slashes at all, it is considered to be an action ID of the current controller and + // will be prepended with uniqueId; + $this->assertEquals('/base/index.php?r=page/edit', Url::toRoute('edit')); + $this->assertEquals('/base/index.php?r=page/edit&id=20', Url::toRoute(['edit', 'id' => 20])); + $this->assertEquals('http://example.com/base/index.php?r=page/edit&id=20', Url::toRoute(['edit', 'id' => 20], true)); + $this->assertEquals('https://example.com/base/index.php?r=page/edit&id=20', Url::toRoute(['edit', 'id' => 20], 'https')); + + // If the route has no leading slash, it is considered to be a route relative + // to the current module and will be prepended with the module's uniqueId. + $this->mockAction('default', 'index', 'stats'); + $this->assertEquals('/base/index.php?r=stats/user/view', Url::toRoute('user/view')); + $this->assertEquals('/base/index.php?r=stats/user/view&id=42', Url::toRoute(['user/view', 'id' => 42])); + $this->assertEquals('http://example.com/base/index.php?r=stats/user/view&id=42', Url::toRoute(['user/view', 'id' => 42], true)); + $this->assertEquals('https://example.com/base/index.php?r=stats/user/view&id=42', Url::toRoute(['user/view', 'id' => 42], 'https')); + + // In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. + $this->removeMockedAction(); + + $this->assertEquals('/base/index.php?r=site/view', Url::toRoute('site/view')); + $this->assertEquals('http://example.com/base/index.php?r=site/view', Url::toRoute('site/view', true)); + $this->assertEquals('https://example.com/base/index.php?r=site/view', Url::toRoute('site/view', 'https')); + $this->assertEquals('/base/index.php?r=site/view&id=37', Url::toRoute(['site/view', 'id' => 37])); + } + + public function testTo() + { + // is an array: the first array element is considered a route, while the rest of the name-value + // pairs are treated as the parameters to be used for URL creation using Url::toRoute. + $this->mockAction('page', 'view', null, ['id' => 10]); + $this->assertEquals('/base/index.php?r=page/edit&id=20', Url::to(['edit', 'id' => 20])); + $this->assertEquals('/base/index.php?r=page/edit', Url::to(['edit'])); + $this->assertEquals('/base/index.php?r=page/view', Url::to([''])); + + $this->assertEquals('http://example.com/base/index.php?r=page/edit&id=20', Url::to(['edit', 'id' => 20], true)); + $this->assertEquals('http://example.com/base/index.php?r=page/edit', Url::to(['edit'], true)); + $this->assertEquals('http://example.com/base/index.php?r=page/view', Url::to([''], true)); + + $this->assertEquals('https://example.com/base/index.php?r=page/edit&id=20', Url::to(['edit', 'id' => 20], 'https')); + $this->assertEquals('https://example.com/base/index.php?r=page/edit', Url::to(['edit'], 'https')); + $this->assertEquals('https://example.com/base/index.php?r=page/view', Url::to([''], 'https')); + + //In case there is no controller, [[\yii\web\UrlManager::createUrl()]] will be used. + $this->removeMockedAction(); + + $this->assertEquals('/base/index.php?r=edit&id=20', Url::to(['edit', 'id' => 20])); + $this->assertEquals('/base/index.php?r=edit', Url::to(['edit'])); + $this->assertEquals('/base/index.php?r=', Url::to([''])); + + $this->assertEquals('http://example.com/base/index.php?r=edit&id=20', Url::to(['edit', 'id' => 20], true)); + $this->assertEquals('http://example.com/base/index.php?r=edit', Url::to(['edit'], true)); + $this->assertEquals('http://example.com/base/index.php?r=', Url::to([''], true)); + + $this->assertEquals('https://example.com/base/index.php?r=edit&id=20', Url::to(['edit', 'id' => 20], 'https')); + $this->assertEquals('https://example.com/base/index.php?r=edit', Url::to(['edit'], 'https')); + $this->assertEquals('https://example.com/base/index.php?r=', Url::to([''], 'https')); + + // is an empty string: the currently requested URL will be returned; + $this->mockAction('page', 'view', null, ['id' => 10]); + $this->assertEquals('/base/index.php&r=site/current&id=42', Url::to('')); + $this->assertEquals('http://example.com/base/index.php&r=site/current&id=42', Url::to('', true)); + $this->assertEquals('https://example.com/base/index.php&r=site/current&id=42', Url::to('', 'https')); + $this->removeMockedAction(); + + // is a non-empty string: it will first be processed by [[Yii::getAlias()]]. If the result + // is an absolute URL, it will be returned either without any change or, if schema was specified, with schema + // replaced; Otherwise, the result will be prefixed with [[\yii\web\Request::baseUrl]] and returned. + \Yii::setAlias('@web1', 'http://test.example.com/test/me1'); + \Yii::setAlias('@web2', 'test/me2'); + \Yii::setAlias('@web3', ''); + \Yii::setAlias('@web4', '/test'); + \Yii::setAlias('@web5', '#test'); + + $this->assertEquals('http://test.example.com/test/me1', Url::to('@web1')); + $this->assertEquals('http://test.example.com/test/me1', Url::to('@web1', true)); + $this->assertEquals('https://test.example.com/test/me1', Url::to('@web1', 'https')); + + $this->assertEquals('/base/test/me2', Url::to('@web2')); + $this->assertEquals('http://example.com/base/test/me2', Url::to('@web2', true)); + $this->assertEquals('https://example.com/base/test/me2', Url::to('@web2', 'https')); + + $this->assertEquals('/base/', Url::to('@web3')); + $this->assertEquals('http://example.com/base/', Url::to('@web3', true)); + $this->assertEquals('https://example.com/base/', Url::to('@web3', 'https')); + + $this->assertEquals('/test', Url::to('@web4')); + $this->assertEquals('http://example.com/test', Url::to('@web4', true)); + $this->assertEquals('https://example.com/test', Url::to('@web4', 'https')); + + $this->assertEquals('#test', Url::to('@web5')); + $this->assertEquals('http://example.com#test', Url::to('@web5', true)); + $this->assertEquals('https://example.com#test', Url::to('@web5', 'https')); + } + + public function testHome() + { + $this->assertEquals('/base/index.php', Url::home()); + $this->assertEquals('http://example.com/base/index.php', Url::home(true)); + $this->assertEquals('https://example.com/base/index.php', Url::home('https')); + } + + public function testCanonical() + { + $this->mockAction('page', 'view', null, ['id' => 10]); + $this->assertEquals('http://example.com/base/index.php?r=page/view&id=10', Url::canonical()); + $this->removeMockedAction(); + } } diff --git a/tests/unit/framework/helpers/VarDumperTest.php b/tests/unit/framework/helpers/VarDumperTest.php index 11fe6d401d3..7000ca74210 100644 --- a/tests/unit/framework/helpers/VarDumperTest.php +++ b/tests/unit/framework/helpers/VarDumperTest.php @@ -9,12 +9,12 @@ */ class VarDumperTest extends TestCase { - public function testDumpObject() - { - $obj = new \StdClass(); - ob_start(); - VarDumper::dump($obj); - $this->assertEquals("stdClass#1\n(\n)", ob_get_contents()); - ob_end_clean(); - } + public function testDumpObject() + { + $obj = new \StdClass(); + ob_start(); + VarDumper::dump($obj); + $this->assertEquals("stdClass#1\n(\n)", ob_get_contents()); + ob_end_clean(); + } } diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index a067f70359f..146a81e73c1 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -17,155 +17,155 @@ */ class FallbackMessageFormatterTest extends TestCase { - const N = 'n'; - const N_VALUE = 42; - const SUBJECT = 'сабж'; - const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; - - public function patterns() - { - return [ - [ - '{'.self::SUBJECT.'} is {'.self::N.'}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - self::SUBJECT => self::SUBJECT_VALUE, - ] - ], - - [ - '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - self::SUBJECT => self::SUBJECT_VALUE, - ] - ], - - [ - '{'.self::SUBJECT.'} is {'.self::N.', number, integer}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - self::SUBJECT => self::SUBJECT_VALUE, - ] - ], - - // This one was provided by Aura.Intl. Thanks! - [<<<_MSG_ + const N = 'n'; + const N_VALUE = 42; + const SUBJECT = 'сабж'; + const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; + + public function patterns() + { + return [ + [ + '{'.self::SUBJECT.'} is {'.self::N.'}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], + + [ + '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], + + [ + '{'.self::SUBJECT.'} is {'.self::N.', number, integer}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], + + // This one was provided by Aura.Intl. Thanks! + [<<<_MSG_ {gender_of_host, select, female {{num_guests, plural, offset:1 - =0 {{host} does not give a party.} - =1 {{host} invites {guest} to her party.} - =2 {{host} invites {guest} and one other person to her party.} - other {{host} invites {guest} and # other people to her party.}}} + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to her party.} + =2 {{host} invites {guest} and one other person to her party.} + other {{host} invites {guest} and # other people to her party.}}} male {{num_guests, plural, offset:1 - =0 {{host} does not give a party.} - =1 {{host} invites {guest} to his party.} - =2 {{host} invites {guest} and one other person to his party.} - other {{host} invites {guest} and # other people to his party.}}} + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to his party.} + =2 {{host} invites {guest} and one other person to his party.} + other {{host} invites {guest} and # other people to his party.}}} other {{num_guests, plural, offset:1 - =0 {{host} does not give a party.} - =1 {{host} invites {guest} to their party.} - =2 {{host} invites {guest} and one other person to their party.} - other {{host} invites {guest} and # other people to their party.}}}} + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to their party.} + =2 {{host} invites {guest} and one other person to their party.} + other {{host} invites {guest} and # other people to their party.}}}} _MSG_ - , - 'ralph invites beep and 3 other people to his party.', - [ - 'gender_of_host' => 'male', - 'num_guests' => 4, - 'host' => 'ralph', - 'guest' => 'beep' - ] - ], - - [ - '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', - 'Alexander is male and he loves Yii!', - [ - 'name' => 'Alexander', - 'gender' => 'male', - ], - ], - - // verify pattern in select does not get replaced - [ - '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', - 'Alexander is male and he loves Yii!', - [ - 'name' => 'Alexander', - 'gender' => 'male', - // following should not be replaced - 'he' => 'wtf', - 'she' => 'wtf', - 'it' => 'wtf', - ] - ], - - // verify pattern in select message gets replaced - [ - '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!', - 'Alexander is male and wtf loves Yii!', - [ - 'name' => 'Alexander', - 'gender' => 'male', - 'he' => 'wtf', - 'she' => 'wtf', - ], - ], - - // some parser specific verifications - [ - '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr} is {gender}!', - 'male and wtf loves 42 is male!', - [ - 'nr' => 42, - 'gender' => 'male', - 'he' => 'wtf', - 'she' => 'wtf', - ], - ], - ]; - } - - /** - * @dataProvider patterns - */ - public function testNamedArguments($pattern, $expected, $args) - { - $formatter = new FallbackMessageFormatter(); - $result = $formatter->fallbackFormat($pattern, $args, 'en-US'); - $this->assertEquals($expected, $result, $formatter->getErrorMessage()); - } - - public function testInsufficientArguments() - { - $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; - - $formatter = new FallbackMessageFormatter(); - $result = $formatter->fallbackFormat('{'.self::SUBJECT.'} is {'.self::N.'}', [ - self::N => self::N_VALUE, - ], 'en-US'); - - $this->assertEquals($expected, $result); - } - - public function testNoParams() - { - $pattern = '{'.self::SUBJECT.'} is '.self::N; - - $formatter = new FallbackMessageFormatter(); - $result = $formatter->fallbackFormat($pattern, [], 'en-US'); - $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); - } + , + 'ralph invites beep and 3 other people to his party.', + [ + 'gender_of_host' => 'male', + 'num_guests' => 4, + 'host' => 'ralph', + 'guest' => 'beep' + ] + ], + + [ + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + [ + 'name' => 'Alexander', + 'gender' => 'male', + ], + ], + + // verify pattern in select does not get replaced + [ + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + [ + 'name' => 'Alexander', + 'gender' => 'male', + // following should not be replaced + 'he' => 'wtf', + 'she' => 'wtf', + 'it' => 'wtf', + ] + ], + + // verify pattern in select message gets replaced + [ + '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!', + 'Alexander is male and wtf loves Yii!', + [ + 'name' => 'Alexander', + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ], + ], + + // some parser specific verifications + [ + '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr} is {gender}!', + 'male and wtf loves 42 is male!', + [ + 'nr' => 42, + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ], + ], + ]; + } + + /** + * @dataProvider patterns + */ + public function testNamedArguments($pattern, $expected, $args) + { + $formatter = new FallbackMessageFormatter(); + $result = $formatter->fallbackFormat($pattern, $args, 'en-US'); + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); + } + + public function testInsufficientArguments() + { + $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; + + $formatter = new FallbackMessageFormatter(); + $result = $formatter->fallbackFormat('{'.self::SUBJECT.'} is {'.self::N.'}', [ + self::N => self::N_VALUE, + ], 'en-US'); + + $this->assertEquals($expected, $result); + } + + public function testNoParams() + { + $pattern = '{'.self::SUBJECT.'} is '.self::N; + + $formatter = new FallbackMessageFormatter(); + $result = $formatter->fallbackFormat($pattern, [], 'en-US'); + $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); + } } class FallbackMessageFormatter extends MessageFormatter { - public function fallbackFormat($pattern, $args, $locale) - { - return parent::fallbackFormat($pattern, $args, $locale); - } + public function fallbackFormat($pattern, $args, $locale) + { + return parent::fallbackFormat($pattern, $args, $locale); + } } diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php index 97e198db445..f457cbca4d5 100644 --- a/tests/unit/framework/i18n/FormatterTest.php +++ b/tests/unit/framework/i18n/FormatterTest.php @@ -17,80 +17,80 @@ */ class FormatterTest extends TestCase { - /** - * @var Formatter - */ - protected $formatter; + /** + * @var Formatter + */ + protected $formatter; - protected function setUp() - { - parent::setUp(); - if (!extension_loaded('intl')) { - $this->markTestSkipped('intl extension is required.'); - } - $this->mockApplication([ - 'timeZone' => 'UTC', - ]); - $this->formatter = new Formatter(['locale' => 'en-US']); - } + protected function setUp() + { + parent::setUp(); + if (!extension_loaded('intl')) { + $this->markTestSkipped('intl extension is required.'); + } + $this->mockApplication([ + 'timeZone' => 'UTC', + ]); + $this->formatter = new Formatter(['locale' => 'en-US']); + } - protected function tearDown() - { - parent::tearDown(); - $this->formatter = null; - } + protected function tearDown() + { + parent::tearDown(); + $this->formatter = null; + } - public function testAsDecimal() - { - $value = '123'; - $this->assertSame($value, $this->formatter->asDecimal($value)); - $value = '123456'; - $this->assertSame("123,456", $this->formatter->asDecimal($value)); - $value = '-123456.123'; - $this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); - } + public function testAsDecimal() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asDecimal($value)); + $value = '123456'; + $this->assertSame("123,456", $this->formatter->asDecimal($value)); + $value = '-123456.123'; + $this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); + } - public function testAsPercent() - { - $value = '123'; - $this->assertSame('12,300%', $this->formatter->asPercent($value)); - $value = '0.1234'; - $this->assertSame("12%", $this->formatter->asPercent($value)); - $value = '-0.009343'; - $this->assertSame("-1%", $this->formatter->asPercent($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null)); - } + public function testAsPercent() + { + $value = '123'; + $this->assertSame('12,300%', $this->formatter->asPercent($value)); + $value = '0.1234'; + $this->assertSame("12%", $this->formatter->asPercent($value)); + $value = '-0.009343'; + $this->assertSame("-1%", $this->formatter->asPercent($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null)); + } - public function testAsScientific() - { - $value = '123'; - $this->assertSame('1.23E2', $this->formatter->asScientific($value)); - $value = '123456'; - $this->assertSame("1.23456E5", $this->formatter->asScientific($value)); - $value = '-123456.123'; - $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); - } + public function testAsScientific() + { + $value = '123'; + $this->assertSame('1.23E2', $this->formatter->asScientific($value)); + $value = '123456'; + $this->assertSame("1.23456E5", $this->formatter->asScientific($value)); + $value = '-123456.123'; + $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); + } - public function testAsCurrency() - { - $value = '123'; - $this->assertSame('$123.00', $this->formatter->asCurrency($value)); - $value = '123.456'; - $this->assertSame("$123.46", $this->formatter->asCurrency($value)); - // Starting from ICU 52.1, negative currency value will be formatted as -$123,456.12 - // see: http://source.icu-project.org/repos/icu/icu/tags/release-52-1/source/data/locales/en.txt + public function testAsCurrency() + { + $value = '123'; + $this->assertSame('$123.00', $this->formatter->asCurrency($value)); + $value = '123.456'; + $this->assertSame("$123.46", $this->formatter->asCurrency($value)); + // Starting from ICU 52.1, negative currency value will be formatted as -$123,456.12 + // see: http://source.icu-project.org/repos/icu/icu/tags/release-52-1/source/data/locales/en.txt // $value = '-123456.123'; // $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); - } + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); + } - public function testDate() - { - $time = time(); - $this->assertSame(date('n/j/y', $time), $this->formatter->asDate($time)); - $this->assertSame(date('F j, Y', $time), $this->formatter->asDate($time, 'long')); - $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); - } + public function testDate() + { + $time = time(); + $this->assertSame(date('n/j/y', $time), $this->formatter->asDate($time)); + $this->assertSame(date('F j, Y', $time), $this->formatter->asDate($time, 'long')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); + } } diff --git a/tests/unit/framework/i18n/GettextMessageSourceTest.php b/tests/unit/framework/i18n/GettextMessageSourceTest.php index 727fee9ead2..cb586de8e3a 100644 --- a/tests/unit/framework/i18n/GettextMessageSourceTest.php +++ b/tests/unit/framework/i18n/GettextMessageSourceTest.php @@ -9,8 +9,8 @@ */ class GettextMessageSourceTest extends TestCase { - public function testLoadMessages() - { - $this->markTestIncomplete(); - } + public function testLoadMessages() + { + $this->markTestIncomplete(); + } } diff --git a/tests/unit/framework/i18n/GettextMoFileTest.php b/tests/unit/framework/i18n/GettextMoFileTest.php index 373fa20901d..b9ef8971ba8 100644 --- a/tests/unit/framework/i18n/GettextMoFileTest.php +++ b/tests/unit/framework/i18n/GettextMoFileTest.php @@ -10,89 +10,89 @@ */ class GettextMoFileTest extends TestCase { - public function testLoad() - { - $moFile = new GettextMoFile(); - $moFilePath = __DIR__ . '/../../data/i18n/test.mo'; - $context1 = $moFile->load($moFilePath, 'context1'); - $context2 = $moFile->load($moFilePath, 'context2'); - - // item count - $this->assertCount(3, $context1); - $this->assertCount(2, $context2); - - // original messages - $this->assertArrayNotHasKey("Missing\n\r\t\"translation.", $context1); - $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); - $this->assertArrayHasKey("String number two.", $context1); - $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); - - $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); - $this->assertArrayHasKey("test1\\ntest2\n\\\ntest3", $context2); - - // translated messages - $this->assertFalse(in_array("", $context1)); - $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); - $this->assertTrue(in_array('Строка номер два.', $context1)); - $this->assertTrue(in_array('Короткий перевод.', $context1)); - - $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); - $this->assertTrue(in_array("тест1\\nтест2\n\\\nтест3", $context2)); - } - - public function testSave() - { - // initial data - $s = chr(4); - $messages = [ - 'Hello!' => 'Привет!', - "context1{$s}Hello?" => 'Привет?', - 'Hello!?' => '', - "context1{$s}Hello!?!" => '', - "context2{$s}\"Quotes\"" => '"Кавычки"', - "context2{$s}\nNew lines\n" => "\nПереносы строк\n", - "context2{$s}\tTabs\t" => "\tТабы\t", - "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", - ]; - - // create temporary directory and dump messages - $poFileDirectory = __DIR__ . '/../../runtime/i18n'; - if (!is_dir($poFileDirectory)) { - mkdir($poFileDirectory); - } - if (is_file($poFileDirectory . '/test.mo')) { - unlink($poFileDirectory . '/test.mo'); - } - - $moFile = new GettextMoFile(); - $moFile->save($poFileDirectory . '/test.mo', $messages); - - // load messages - $context1 = $moFile->load($poFileDirectory . '/test.mo', 'context1'); - $context2 = $moFile->load($poFileDirectory . '/test.mo', 'context2'); - - // context1 - $this->assertCount(2, $context1); - - $this->assertArrayHasKey('Hello?', $context1); - $this->assertTrue(in_array('Привет?', $context1)); - - $this->assertArrayHasKey('Hello!?!', $context1); - $this->assertTrue(in_array('', $context1)); - - // context2 - $this->assertCount(4, $context2); - - $this->assertArrayHasKey("\"Quotes\"", $context2); - $this->assertTrue(in_array('"Кавычки"', $context2)); - - $this->assertArrayHasKey("\nNew lines\n", $context2); - $this->assertTrue(in_array("\nПереносы строк\n", $context2)); - - $this->assertArrayHasKey("\tTabs\t", $context2); - $this->assertTrue(in_array("\tТабы\t", $context2)); - - $this->assertArrayHasKey("\rCarriage returns\r", $context2); - $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); - } + public function testLoad() + { + $moFile = new GettextMoFile(); + $moFilePath = __DIR__ . '/../../data/i18n/test.mo'; + $context1 = $moFile->load($moFilePath, 'context1'); + $context2 = $moFile->load($moFilePath, 'context2'); + + // item count + $this->assertCount(3, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayNotHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\ntest2\n\\\ntest3", $context2); + + // translated messages + $this->assertFalse(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\nтест2\n\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = [ + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ]; + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.mo')) { + unlink($poFileDirectory . '/test.mo'); + } + + $moFile = new GettextMoFile(); + $moFile->save($poFileDirectory . '/test.mo', $messages); + + // load messages + $context1 = $moFile->load($poFileDirectory . '/test.mo', 'context1'); + $context2 = $moFile->load($poFileDirectory . '/test.mo', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } } diff --git a/tests/unit/framework/i18n/GettextPoFileTest.php b/tests/unit/framework/i18n/GettextPoFileTest.php index 29db141234e..42aa24ad6b1 100644 --- a/tests/unit/framework/i18n/GettextPoFileTest.php +++ b/tests/unit/framework/i18n/GettextPoFileTest.php @@ -10,89 +10,89 @@ */ class GettextPoFileTest extends TestCase { - public function testLoad() - { - $poFile = new GettextPoFile(); - $poFilePath = __DIR__ . '/../../data/i18n/test.po'; - $context1 = $poFile->load($poFilePath, 'context1'); - $context2 = $poFile->load($poFilePath, 'context2'); - - // item count - $this->assertCount(4, $context1); - $this->assertCount(2, $context2); - - // original messages - $this->assertArrayHasKey("Missing\n\r\t\"translation.", $context1); - $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); - $this->assertArrayHasKey("String number two.", $context1); - $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); - - $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); - $this->assertArrayHasKey("test1\\\ntest2\n\\\\\ntest3", $context2); - - // translated messages - $this->assertTrue(in_array("", $context1)); - $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); - $this->assertTrue(in_array('Строка номер два.', $context1)); - $this->assertTrue(in_array('Короткий перевод.', $context1)); - - $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); - $this->assertTrue(in_array("тест1\\\nтест2\n\\\\\nтест3", $context2)); - } - - public function testSave() - { - // initial data - $s = chr(4); - $messages = [ - 'Hello!' => 'Привет!', - "context1{$s}Hello?" => 'Привет?', - 'Hello!?' => '', - "context1{$s}Hello!?!" => '', - "context2{$s}\"Quotes\"" => '"Кавычки"', - "context2{$s}\nNew lines\n" => "\nПереносы строк\n", - "context2{$s}\tTabs\t" => "\tТабы\t", - "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", - ]; - - // create temporary directory and dump messages - $poFileDirectory = __DIR__ . '/../../runtime/i18n'; - if (!is_dir($poFileDirectory)) { - mkdir($poFileDirectory); - } - if (is_file($poFileDirectory . '/test.po')) { - unlink($poFileDirectory . '/test.po'); - } - - $poFile = new GettextPoFile(); - $poFile->save($poFileDirectory . '/test.po', $messages); - - // load messages - $context1 = $poFile->load($poFileDirectory . '/test.po', 'context1'); - $context2 = $poFile->load($poFileDirectory . '/test.po', 'context2'); - - // context1 - $this->assertCount(2, $context1); - - $this->assertArrayHasKey('Hello?', $context1); - $this->assertTrue(in_array('Привет?', $context1)); - - $this->assertArrayHasKey('Hello!?!', $context1); - $this->assertTrue(in_array('', $context1)); - - // context2 - $this->assertCount(4, $context2); - - $this->assertArrayHasKey("\"Quotes\"", $context2); - $this->assertTrue(in_array('"Кавычки"', $context2)); - - $this->assertArrayHasKey("\nNew lines\n", $context2); - $this->assertTrue(in_array("\nПереносы строк\n", $context2)); - - $this->assertArrayHasKey("\tTabs\t", $context2); - $this->assertTrue(in_array("\tТабы\t", $context2)); - - $this->assertArrayHasKey("\rCarriage returns\r", $context2); - $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); - } + public function testLoad() + { + $poFile = new GettextPoFile(); + $poFilePath = __DIR__ . '/../../data/i18n/test.po'; + $context1 = $poFile->load($poFilePath, 'context1'); + $context2 = $poFile->load($poFilePath, 'context2'); + + // item count + $this->assertCount(4, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\\ntest2\n\\\\\ntest3", $context2); + + // translated messages + $this->assertTrue(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\\nтест2\n\\\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = [ + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ]; + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.po')) { + unlink($poFileDirectory . '/test.po'); + } + + $poFile = new GettextPoFile(); + $poFile->save($poFileDirectory . '/test.po', $messages); + + // load messages + $context1 = $poFile->load($poFileDirectory . '/test.po', 'context1'); + $context2 = $poFile->load($poFileDirectory . '/test.po', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } } diff --git a/tests/unit/framework/i18n/I18NTest.php b/tests/unit/framework/i18n/I18NTest.php index 68b0aed84f9..ab018404ffd 100644 --- a/tests/unit/framework/i18n/I18NTest.php +++ b/tests/unit/framework/i18n/I18NTest.php @@ -8,7 +8,6 @@ namespace yiiunit\framework\i18n; use yii\base\Event; -use yii\base\Model; use yii\i18n\I18N; use yii\i18n\PhpMessageSource; use yiiunit\TestCase; @@ -20,107 +19,107 @@ */ class I18NTest extends TestCase { - /** - * @var I18N - */ - public $i18n; - - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $this->i18n = new I18N([ - 'translations' => [ - 'test' => new PhpMessageSource([ - 'basePath' => '@yiiunit/data/i18n/messages', - ]) - ] - ]); - } - - public function testTranslate() - { - $msg = 'The dog runs fast.'; - - // source = target. Should be returned as is. - $this->assertEquals('The dog runs fast.', $this->i18n->translate('test', $msg, [], 'en')); - - // exact match - $this->assertEquals('Der Hund rennt schnell.', $this->i18n->translate('test', $msg, [], 'de-DE')); - - // fallback to just language code with absent exact match - $this->assertEquals('Собака бегает быстро.', $this->i18n->translate('test', $msg, [], 'ru-RU')); - - // fallback to just langauge code with present exact match - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - } - - public function testTranslateParams() - { - $msg = 'His speed is about {n} km/h.'; - $params = ['n' => 42]; - $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en-US')); - $this->assertEquals('Seine Geschwindigkeit beträgt 42 km/h.', $this->i18n->translate('test', $msg, $params, 'de-DE')); - } - - public function testTranslateParams2() - { - if (!extension_loaded("intl")) { - $this->markTestSkipped("intl not installed. Skipping."); - } - $msg = 'His name is {name} and his speed is about {n, number} km/h.'; - $params = [ - 'n' => 42, - 'name' => 'DA VINCI', // http://petrix.com/dognames/d.html - ]; - $this->assertEquals('His name is DA VINCI and his speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en-US')); - $this->assertEquals('Er heißt DA VINCI und ist 42 km/h schnell.', $this->i18n->translate('test', $msg, $params, 'de-DE')); - } - - public function testSpecialParams() - { - $msg = 'His speed is about {0} km/h.'; - - $this->assertEquals('His speed is about 0 km/h.', $this->i18n->translate('test', $msg, 0, 'en-US')); - $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, 42, 'en-US')); - $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, null, 'en-US')); - $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, [], 'en-US')); - } - - /** - * When translation is missing source language should be used for formatting. - * https://github.com/yiisoft/yii2/issues/2209 - */ - public function testMissingTranslationFormatting() - { - $this->assertEquals('1 item', $this->i18n->translate('test', '{0, number} {0, plural, one{item} other{items}}', 1, 'hu')); - } - - /** - * https://github.com/yiisoft/yii2/issues/2519 - */ - public function testMissingTranslationEvent() - { - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - $this->assertEquals('Missing translation message.', $this->i18n->translate('test', 'Missing translation message.', [], 'de-DE')); - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - - Event::on(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION, function ($event) {}); - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - $this->assertEquals('Missing translation message.', $this->i18n->translate('test', 'Missing translation message.', [], 'de-DE')); - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - Event::off(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION); - - Event::on(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION, function ($event) { - if ($event->message == 'New missing translation message.') { - $event->translatedMessage = 'TRANSLATION MISSING HERE!'; - } - }); - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - $this->assertEquals('Another missing translation message.', $this->i18n->translate('test', 'Another missing translation message.', [], 'de-DE')); - $this->assertEquals('Missing translation message.', $this->i18n->translate('test', 'Missing translation message.', [], 'de-DE')); - $this->assertEquals('TRANSLATION MISSING HERE!', $this->i18n->translate('test', 'New missing translation message.', [], 'de-DE')); - $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); - Event::off(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION); - } + /** + * @var I18N + */ + public $i18n; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->i18n = new I18N([ + 'translations' => [ + 'test' => new PhpMessageSource([ + 'basePath' => '@yiiunit/data/i18n/messages', + ]) + ] + ]); + } + + public function testTranslate() + { + $msg = 'The dog runs fast.'; + + // source = target. Should be returned as is. + $this->assertEquals('The dog runs fast.', $this->i18n->translate('test', $msg, [], 'en')); + + // exact match + $this->assertEquals('Der Hund rennt schnell.', $this->i18n->translate('test', $msg, [], 'de-DE')); + + // fallback to just language code with absent exact match + $this->assertEquals('Собака бегает быстро.', $this->i18n->translate('test', $msg, [], 'ru-RU')); + + // fallback to just langauge code with present exact match + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + } + + public function testTranslateParams() + { + $msg = 'His speed is about {n} km/h.'; + $params = ['n' => 42]; + $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en-US')); + $this->assertEquals('Seine Geschwindigkeit beträgt 42 km/h.', $this->i18n->translate('test', $msg, $params, 'de-DE')); + } + + public function testTranslateParams2() + { + if (!extension_loaded("intl")) { + $this->markTestSkipped("intl not installed. Skipping."); + } + $msg = 'His name is {name} and his speed is about {n, number} km/h.'; + $params = [ + 'n' => 42, + 'name' => 'DA VINCI', // http://petrix.com/dognames/d.html + ]; + $this->assertEquals('His name is DA VINCI and his speed is about 42 km/h.', $this->i18n->translate('test', $msg, $params, 'en-US')); + $this->assertEquals('Er heißt DA VINCI und ist 42 km/h schnell.', $this->i18n->translate('test', $msg, $params, 'de-DE')); + } + + public function testSpecialParams() + { + $msg = 'His speed is about {0} km/h.'; + + $this->assertEquals('His speed is about 0 km/h.', $this->i18n->translate('test', $msg, 0, 'en-US')); + $this->assertEquals('His speed is about 42 km/h.', $this->i18n->translate('test', $msg, 42, 'en-US')); + $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, null, 'en-US')); + $this->assertEquals('His speed is about {0} km/h.', $this->i18n->translate('test', $msg, [], 'en-US')); + } + + /** + * When translation is missing source language should be used for formatting. + * https://github.com/yiisoft/yii2/issues/2209 + */ + public function testMissingTranslationFormatting() + { + $this->assertEquals('1 item', $this->i18n->translate('test', '{0, number} {0, plural, one{item} other{items}}', 1, 'hu')); + } + + /** + * https://github.com/yiisoft/yii2/issues/2519 + */ + public function testMissingTranslationEvent() + { + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + $this->assertEquals('Missing translation message.', $this->i18n->translate('test', 'Missing translation message.', [], 'de-DE')); + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + + Event::on(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION, function ($event) {}); + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + $this->assertEquals('Missing translation message.', $this->i18n->translate('test', 'Missing translation message.', [], 'de-DE')); + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + Event::off(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION); + + Event::on(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION, function ($event) { + if ($event->message == 'New missing translation message.') { + $event->translatedMessage = 'TRANSLATION MISSING HERE!'; + } + }); + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + $this->assertEquals('Another missing translation message.', $this->i18n->translate('test', 'Another missing translation message.', [], 'de-DE')); + $this->assertEquals('Missing translation message.', $this->i18n->translate('test', 'Missing translation message.', [], 'de-DE')); + $this->assertEquals('TRANSLATION MISSING HERE!', $this->i18n->translate('test', 'New missing translation message.', [], 'de-DE')); + $this->assertEquals('Hallo Welt!', $this->i18n->translate('test', 'Hello world!', [], 'de-DE')); + Event::off(PhpMessageSource::className(), PhpMessageSource::EVENT_MISSING_TRANSLATION); + } } diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 1d5d0071f3c..85f0c52205f 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -17,321 +17,321 @@ */ class MessageFormatterTest extends TestCase { - const N = 'n'; - const N_VALUE = 42; - const SUBJECT = 'сабж'; - const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; + const N = 'n'; + const N_VALUE = 42; + const SUBJECT = 'сабж'; + const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; - public function patterns() - { - return [ - [ - '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - self::SUBJECT => self::SUBJECT_VALUE, - ] - ], + public function patterns() + { + return [ + [ + '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], - [ - '{'.self::SUBJECT.'} is {'.self::N.', number, integer}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - self::SUBJECT => self::SUBJECT_VALUE, - ] - ], + [ + '{'.self::SUBJECT.'} is {'.self::N.', number, integer}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], - // This one was provided by Aura.Intl. Thanks! - [<<<_MSG_ + // This one was provided by Aura.Intl. Thanks! + [<<<_MSG_ {gender_of_host, select, female {{num_guests, plural, offset:1 - =0 {{host} does not give a party.} - =1 {{host} invites {guest} to her party.} - =2 {{host} invites {guest} and one other person to her party.} - other {{host} invites {guest} and # other people to her party.}}} + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to her party.} + =2 {{host} invites {guest} and one other person to her party.} + other {{host} invites {guest} and # other people to her party.}}} male {{num_guests, plural, offset:1 - =0 {{host} does not give a party.} - =1 {{host} invites {guest} to his party.} - =2 {{host} invites {guest} and one other person to his party.} - other {{host} invites {guest} and # other people to his party.}}} + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to his party.} + =2 {{host} invites {guest} and one other person to his party.} + other {{host} invites {guest} and # other people to his party.}}} other {{num_guests, plural, offset:1 - =0 {{host} does not give a party.} - =1 {{host} invites {guest} to their party.} - =2 {{host} invites {guest} and one other person to their party.} - other {{host} invites {guest} and # other people to their party.}}}} + =0 {{host} does not give a party.} + =1 {{host} invites {guest} to their party.} + =2 {{host} invites {guest} and one other person to their party.} + other {{host} invites {guest} and # other people to their party.}}}} _MSG_ - , - 'ralph invites beep and 3 other people to his party.', - [ - 'gender_of_host' => 'male', - 'num_guests' => 4, - 'host' => 'ralph', - 'guest' => 'beep' - ], - defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.8', '<'), - 'select format is available in ICU > 4.4 and plural format with =X selector is avilable since 4.8' - ], + , + 'ralph invites beep and 3 other people to his party.', + [ + 'gender_of_host' => 'male', + 'num_guests' => 4, + 'host' => 'ralph', + 'guest' => 'beep' + ], + defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.8', '<'), + 'select format is available in ICU > 4.4 and plural format with =X selector is avilable since 4.8' + ], - [ - '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', - 'Alexander is male and he loves Yii!', - [ - 'name' => 'Alexander', - 'gender' => 'male', - ], - defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.4.2', '<'), - 'select format is available in ICU > 4.4' - ], + [ + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + [ + 'name' => 'Alexander', + 'gender' => 'male', + ], + defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.4.2', '<'), + 'select format is available in ICU > 4.4' + ], - // verify pattern in select does not get replaced - [ - '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', - 'Alexander is male and he loves Yii!', - [ - 'name' => 'Alexander', - 'gender' => 'male', - // following should not be replaced - 'he' => 'wtf', - 'she' => 'wtf', - 'it' => 'wtf', - ], - defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.4.2', '<'), - 'select format is available in ICU > 4.4' - ], + // verify pattern in select does not get replaced + [ + '{name} is {gender} and {gender, select, female{she} male{he} other{it}} loves Yii!', + 'Alexander is male and he loves Yii!', + [ + 'name' => 'Alexander', + 'gender' => 'male', + // following should not be replaced + 'he' => 'wtf', + 'she' => 'wtf', + 'it' => 'wtf', + ], + defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.4.2', '<'), + 'select format is available in ICU > 4.4' + ], - // verify pattern in select message gets replaced - [ - '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!', - 'Alexander is male and wtf loves Yii!', - [ - 'name' => 'Alexander', - 'gender' => 'male', - 'he' => 'wtf', - 'she' => 'wtf', - ], - defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.8', '<'), - 'parameters in select format do not seem to work in ICU < 4.8' - ], + // verify pattern in select message gets replaced + [ + '{name} is {gender} and {gender, select, female{she} male{{he}} other{it}} loves Yii!', + 'Alexander is male and wtf loves Yii!', + [ + 'name' => 'Alexander', + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ], + defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.8', '<'), + 'parameters in select format do not seem to work in ICU < 4.8' + ], - // some parser specific verifications - [ - '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr, number} is {gender}!', - 'male and wtf loves 42 is male!', - [ - 'nr' => 42, - 'gender' => 'male', - 'he' => 'wtf', - 'she' => 'wtf', - ], - defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.4.2', '<'), - 'select format is available in ICU > 4.4' - ], + // some parser specific verifications + [ + '{gender} and {gender, select, female{she} male{{he}} other{it}} loves {nr, number} is {gender}!', + 'male and wtf loves 42 is male!', + [ + 'nr' => 42, + 'gender' => 'male', + 'he' => 'wtf', + 'she' => 'wtf', + ], + defined('INTL_ICU_VERSION') && version_compare(INTL_ICU_VERSION, '4.4.2', '<'), + 'select format is available in ICU > 4.4' + ], - // test ICU version compatibility - [ - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', - [], - ], - [ - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', - 'Showing 1-10 of 12 items.', - [// A - 'begin' => 1, - 'end' => 10, - 'count' => 10, - 'totalCount' => 12, - 'page' => 1, - 'pageCount' => 2, - ] - ], - [ - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', - 'Showing 1-1 of 1 item.', - [// B - 'begin' => 1, - 'end' => 1, - 'count' => 1, - 'totalCount' => 1, - 'page' => 1, - 'pageCount' => 1, - ] - ], - [ - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', - 'Showing 0-0 of 0 items.', - [// C - 'begin' => 0, - 'end' => 0, - 'count' => 0, - 'totalCount' => 0, - 'page' => 1, - 'pageCount' => 1, - ] - ], - [ - 'Total {count, number} {count, plural, one{item} other{items}}.', - 'Total {count, number} {count, plural, one{item} other{items}}.', - [] - ], - [ - 'Total {count, number} {count, plural, one{item} other{items}}.', - 'Total 1 item.', - [ - 'count' => 1, - ] - ], - [ - 'Total {count, number} {count, plural, one{item} other{items}}.', - 'Total 1 item.', - [ - 'begin' => 5, - 'count' => 1, - 'end' => 10, - ] - ], - [ - '{0, plural, one {offer} other {offers}}', - '{0, plural, one {offer} other {offers}}', - [], - ], - [ - '{0, plural, one {offer} other {offers}}', - 'offers', - [0], - ], - [ - '{0, plural, one {offer} other {offers}}', - 'offer', - [1], - ], - [ - '{0, plural, one {offer} other {offers}}', - 'offers', - [13], - ], - ]; - } + // test ICU version compatibility + [ + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', + [], + ], + [ + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', + 'Showing 1-10 of 12 items.', + [// A + 'begin' => 1, + 'end' => 10, + 'count' => 10, + 'totalCount' => 12, + 'page' => 1, + 'pageCount' => 2, + ] + ], + [ + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', + 'Showing 1-1 of 1 item.', + [// B + 'begin' => 1, + 'end' => 1, + 'count' => 1, + 'totalCount' => 1, + 'page' => 1, + 'pageCount' => 1, + ] + ], + [ + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.', + 'Showing 0-0 of 0 items.', + [// C + 'begin' => 0, + 'end' => 0, + 'count' => 0, + 'totalCount' => 0, + 'page' => 1, + 'pageCount' => 1, + ] + ], + [ + 'Total {count, number} {count, plural, one{item} other{items}}.', + 'Total {count, number} {count, plural, one{item} other{items}}.', + [] + ], + [ + 'Total {count, number} {count, plural, one{item} other{items}}.', + 'Total 1 item.', + [ + 'count' => 1, + ] + ], + [ + 'Total {count, number} {count, plural, one{item} other{items}}.', + 'Total 1 item.', + [ + 'begin' => 5, + 'count' => 1, + 'end' => 10, + ] + ], + [ + '{0, plural, one {offer} other {offers}}', + '{0, plural, one {offer} other {offers}}', + [], + ], + [ + '{0, plural, one {offer} other {offers}}', + 'offers', + [0], + ], + [ + '{0, plural, one {offer} other {offers}}', + 'offer', + [1], + ], + [ + '{0, plural, one {offer} other {offers}}', + 'offers', + [13], + ], + ]; + } - public function parsePatterns() - { - return [ - [ - self::SUBJECT_VALUE.' is {0, number}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - 0 => self::N_VALUE, - ] - ], + public function parsePatterns() + { + return [ + [ + self::SUBJECT_VALUE.' is {0, number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + 0 => self::N_VALUE, + ] + ], - [ - self::SUBJECT_VALUE.' is {'.self::N.', number}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - ] - ], + [ + self::SUBJECT_VALUE.' is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + ] + ], - [ - self::SUBJECT_VALUE.' is {'.self::N.', number, integer}', // pattern - self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected - [ // params - self::N => self::N_VALUE, - ] - ], + [ + self::SUBJECT_VALUE.' is {'.self::N.', number, integer}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + ] + ], - [ - "{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree", - "4,560 monkeys on 123 trees make 37.073 monkeys per tree", - [ - 0 => 4560, - 1 => 123, - 2 => 37.073 - ], - 'en-US' - ], + [ + "{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree", + "4,560 monkeys on 123 trees make 37.073 monkeys per tree", + [ + 0 => 4560, + 1 => 123, + 2 => 37.073 + ], + 'en-US' + ], - [ - "{0,number,integer} Affen auf {1,number,integer} Bäumen sind {2,number} Affen pro Baum", - "4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum", - [ - 0 => 4560, - 1 => 123, - 2 => 37.073 - ], - 'de', - ], + [ + "{0,number,integer} Affen auf {1,number,integer} Bäumen sind {2,number} Affen pro Baum", + "4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum", + [ + 0 => 4560, + 1 => 123, + 2 => 37.073 + ], + 'de', + ], - [ - "{monkeyCount,number,integer} monkeys on {trees,number,integer} trees make {monkeysPerTree,number} monkeys per tree", - "4,560 monkeys on 123 trees make 37.073 monkeys per tree", - [ - 'monkeyCount' => 4560, - 'trees' => 123, - 'monkeysPerTree' => 37.073 - ], - 'en-US' - ], + [ + "{monkeyCount,number,integer} monkeys on {trees,number,integer} trees make {monkeysPerTree,number} monkeys per tree", + "4,560 monkeys on 123 trees make 37.073 monkeys per tree", + [ + 'monkeyCount' => 4560, + 'trees' => 123, + 'monkeysPerTree' => 37.073 + ], + 'en-US' + ], - [ - "{monkeyCount,number,integer} Affen auf {trees,number,integer} Bäumen sind {monkeysPerTree,number} Affen pro Baum", - "4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum", - [ - 'monkeyCount' => 4560, - 'trees' => 123, - 'monkeysPerTree' => 37.073 - ], - 'de', - ], - ]; - } + [ + "{monkeyCount,number,integer} Affen auf {trees,number,integer} Bäumen sind {monkeysPerTree,number} Affen pro Baum", + "4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum", + [ + 'monkeyCount' => 4560, + 'trees' => 123, + 'monkeysPerTree' => 37.073 + ], + 'de', + ], + ]; + } - /** - * @dataProvider patterns - */ - public function testNamedArguments($pattern, $expected, $args, $skip = false, $skipMessage = '') - { - if ($skip) { - $this->markTestSkipped($skipMessage); - } - $formatter = new MessageFormatter(); - $result = $formatter->format($pattern, $args, 'en-US'); - $this->assertEquals($expected, $result, $formatter->getErrorMessage()); - } + /** + * @dataProvider patterns + */ + public function testNamedArguments($pattern, $expected, $args, $skip = false, $skipMessage = '') + { + if ($skip) { + $this->markTestSkipped($skipMessage); + } + $formatter = new MessageFormatter(); + $result = $formatter->format($pattern, $args, 'en-US'); + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); + } - /** - * @dataProvider parsePatterns - */ - public function testParseNamedArguments($pattern, $expected, $args, $locale = 'en-US') - { - if (!extension_loaded("intl")) { - $this->markTestSkipped("intl not installed. Skipping."); - } + /** + * @dataProvider parsePatterns + */ + public function testParseNamedArguments($pattern, $expected, $args, $locale = 'en-US') + { + if (!extension_loaded("intl")) { + $this->markTestSkipped("intl not installed. Skipping."); + } - $formatter = new MessageFormatter(); - $result = $formatter->parse($pattern, $expected, $locale); - $this->assertEquals($args, $result, $formatter->getErrorMessage() . ' Pattern: ' . $pattern); - } + $formatter = new MessageFormatter(); + $result = $formatter->parse($pattern, $expected, $locale); + $this->assertEquals($args, $result, $formatter->getErrorMessage() . ' Pattern: ' . $pattern); + } - public function testInsufficientArguments() - { - $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; + public function testInsufficientArguments() + { + $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; - $formatter = new MessageFormatter(); - $result = $formatter->format('{'.self::SUBJECT.'} is {'.self::N.', number}', [ - self::N => self::N_VALUE, - ], 'en-US'); + $formatter = new MessageFormatter(); + $result = $formatter->format('{'.self::SUBJECT.'} is {'.self::N.', number}', [ + self::N => self::N_VALUE, + ], 'en-US'); - $this->assertEquals($expected, $result, $formatter->getErrorMessage()); - } + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); + } - public function testNoParams() - { - $pattern = '{'.self::SUBJECT.'} is '.self::N; - $formatter = new MessageFormatter(); - $result = $formatter->format($pattern, [], 'en-US'); - $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); - } + public function testNoParams() + { + $pattern = '{'.self::SUBJECT.'} is '.self::N; + $formatter = new MessageFormatter(); + $result = $formatter->format($pattern, [], 'en-US'); + $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); + } } diff --git a/tests/unit/framework/log/LoggerTest.php b/tests/unit/framework/log/LoggerTest.php index 9ecb34b2a71..558179db41b 100644 --- a/tests/unit/framework/log/LoggerTest.php +++ b/tests/unit/framework/log/LoggerTest.php @@ -5,27 +5,26 @@ namespace yiiunit\framework\log; - use yii\log\Logger; use yiiunit\TestCase; class LoggerTest extends TestCase { - public function testLog() - { - $logger = new Logger(); + public function testLog() + { + $logger = new Logger(); - $logger->log('test1', Logger::LEVEL_INFO); - $this->assertEquals(1, count($logger->messages)); - $this->assertEquals('test1', $logger->messages[0][0]); - $this->assertEquals(Logger::LEVEL_INFO, $logger->messages[0][1]); - $this->assertEquals('application', $logger->messages[0][2]); + $logger->log('test1', Logger::LEVEL_INFO); + $this->assertEquals(1, count($logger->messages)); + $this->assertEquals('test1', $logger->messages[0][0]); + $this->assertEquals(Logger::LEVEL_INFO, $logger->messages[0][1]); + $this->assertEquals('application', $logger->messages[0][2]); - $logger->log('test2', Logger::LEVEL_ERROR, 'category'); - $this->assertEquals(2, count($logger->messages)); - $this->assertEquals('test2', $logger->messages[1][0]); - $this->assertEquals(Logger::LEVEL_ERROR, $logger->messages[1][1]); - $this->assertEquals('category', $logger->messages[1][2]); - } + $logger->log('test2', Logger::LEVEL_ERROR, 'category'); + $this->assertEquals(2, count($logger->messages)); + $this->assertEquals('test2', $logger->messages[1][0]); + $this->assertEquals(Logger::LEVEL_ERROR, $logger->messages[1][1]); + $this->assertEquals('category', $logger->messages[1][2]); + } } diff --git a/tests/unit/framework/log/TargetTest.php b/tests/unit/framework/log/TargetTest.php index e060665917c..035d639a0a6 100644 --- a/tests/unit/framework/log/TargetTest.php +++ b/tests/unit/framework/log/TargetTest.php @@ -5,84 +5,83 @@ namespace yiiunit\framework\log; - use yii\log\Logger; use yii\log\Target; use yiiunit\TestCase; class TargetTest extends TestCase { - public static $messages; + public static $messages; - public function filters() - { - return [ - [[], ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']], + public function filters() + { + return [ + [[], ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']], - [['levels' => 0], ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']], - [ - ['levels' => Logger::LEVEL_INFO | Logger::LEVEL_WARNING | Logger::LEVEL_ERROR | Logger::LEVEL_TRACE], - ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] - ], - [['levels' => ['error']], ['B', 'G', 'H']], - [['levels' => Logger::LEVEL_ERROR], ['B', 'G', 'H']], - [['levels' => ['error', 'warning']], ['B', 'C', 'G', 'H']], - [['levels' => Logger::LEVEL_ERROR | Logger::LEVEL_WARNING], ['B', 'C', 'G', 'H']], + [['levels' => 0], ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']], + [ + ['levels' => Logger::LEVEL_INFO | Logger::LEVEL_WARNING | Logger::LEVEL_ERROR | Logger::LEVEL_TRACE], + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + ], + [['levels' => ['error']], ['B', 'G', 'H']], + [['levels' => Logger::LEVEL_ERROR], ['B', 'G', 'H']], + [['levels' => ['error', 'warning']], ['B', 'C', 'G', 'H']], + [['levels' => Logger::LEVEL_ERROR | Logger::LEVEL_WARNING], ['B', 'C', 'G', 'H']], - [['categories' => ['application']], ['A', 'B', 'C', 'D', 'E']], - [['categories' => ['application*']], ['A', 'B', 'C', 'D', 'E', 'F']], - [['categories' => ['application.*']], ['F']], - [['categories' => ['application.components']], []], - [['categories' => ['application.components.Test']], ['F']], - [['categories' => ['application.components.*']], ['F']], - [['categories' => ['application.*', 'yii.db.*']], ['F', 'G', 'H']], - [['categories' => ['application.*', 'yii.db.*'], 'except' => ['yii.db.Command.*']], ['F', 'G']], + [['categories' => ['application']], ['A', 'B', 'C', 'D', 'E']], + [['categories' => ['application*']], ['A', 'B', 'C', 'D', 'E', 'F']], + [['categories' => ['application.*']], ['F']], + [['categories' => ['application.components']], []], + [['categories' => ['application.components.Test']], ['F']], + [['categories' => ['application.components.*']], ['F']], + [['categories' => ['application.*', 'yii.db.*']], ['F', 'G', 'H']], + [['categories' => ['application.*', 'yii.db.*'], 'except' => ['yii.db.Command.*']], ['F', 'G']], - [['categories' => ['application', 'yii.db.*'], 'levels' => Logger::LEVEL_ERROR], ['B', 'G', 'H']], - [['categories' => ['application'], 'levels' => Logger::LEVEL_ERROR], ['B']], - [['categories' => ['application'], 'levels' => Logger::LEVEL_ERROR | Logger::LEVEL_WARNING], ['B', 'C']], - ]; - } + [['categories' => ['application', 'yii.db.*'], 'levels' => Logger::LEVEL_ERROR], ['B', 'G', 'H']], + [['categories' => ['application'], 'levels' => Logger::LEVEL_ERROR], ['B']], + [['categories' => ['application'], 'levels' => Logger::LEVEL_ERROR | Logger::LEVEL_WARNING], ['B', 'C']], + ]; + } - /** - * @dataProvider filters - */ - public function testFilter($filter, $expected) - { - static::$messages = []; + /** + * @dataProvider filters + */ + public function testFilter($filter, $expected) + { + static::$messages = []; - $logger = new Logger([ - 'targets' => [new TestTarget(array_merge($filter, ['logVars' => []]))], - 'flushInterval' => 1, - ]); - $logger->log('testA', Logger::LEVEL_INFO); - $logger->log('testB', Logger::LEVEL_ERROR); - $logger->log('testC', Logger::LEVEL_WARNING); - $logger->log('testD', Logger::LEVEL_TRACE); - $logger->log('testE', Logger::LEVEL_INFO, 'application'); - $logger->log('testF', Logger::LEVEL_INFO, 'application.components.Test'); - $logger->log('testG', Logger::LEVEL_ERROR, 'yii.db.Command'); - $logger->log('testH', Logger::LEVEL_ERROR, 'yii.db.Command.whatever'); + $logger = new Logger([ + 'targets' => [new TestTarget(array_merge($filter, ['logVars' => []]))], + 'flushInterval' => 1, + ]); + $logger->log('testA', Logger::LEVEL_INFO); + $logger->log('testB', Logger::LEVEL_ERROR); + $logger->log('testC', Logger::LEVEL_WARNING); + $logger->log('testD', Logger::LEVEL_TRACE); + $logger->log('testE', Logger::LEVEL_INFO, 'application'); + $logger->log('testF', Logger::LEVEL_INFO, 'application.components.Test'); + $logger->log('testG', Logger::LEVEL_ERROR, 'yii.db.Command'); + $logger->log('testH', Logger::LEVEL_ERROR, 'yii.db.Command.whatever'); - $this->assertEquals(count($expected), count(static::$messages)); - $i = 0; - foreach ($expected as $e) { - $this->assertEquals('test' . $e, static::$messages[$i++][0]); - } - } + $this->assertEquals(count($expected), count(static::$messages)); + $i = 0; + foreach ($expected as $e) { + $this->assertEquals('test' . $e, static::$messages[$i++][0]); + } + } } class TestTarget extends Target { - public $exportInterval = 1; + public $exportInterval = 1; - /** - * Exports log [[messages]] to a specific destination. - * Child classes must implement this method. - */ - public function export() - { - TargetTest::$messages = array_merge(TargetTest::$messages, $this->messages); - $this->messages = []; - } + /** + * Exports log [[messages]] to a specific destination. + * Child classes must implement this method. + */ + public function export() + { + TargetTest::$messages = array_merge(TargetTest::$messages, $this->messages); + $this->messages = []; + } } diff --git a/tests/unit/framework/mail/BaseMailerTest.php b/tests/unit/framework/mail/BaseMailerTest.php index 0ed455e7e9f..d5baf47bd36 100644 --- a/tests/unit/framework/mail/BaseMailerTest.php +++ b/tests/unit/framework/mail/BaseMailerTest.php @@ -14,232 +14,233 @@ */ class BaseMailerTest extends TestCase { - public function setUp() - { - $this->mockApplication([ - 'components' => [ - 'mail' => $this->createTestMailComponent(), - ] - ]); - $filePath = $this->getTestFilePath(); - if (!file_exists($filePath)) { - FileHelper::createDirectory($filePath); - } - } - - public function tearDown() - { - $filePath = $this->getTestFilePath(); - if (file_exists($filePath)) { - FileHelper::removeDirectory($filePath); - } - } - - /** - * @return string test file path. - */ - protected function getTestFilePath() - { - return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid(); - } - - /** - * @return Mailer test email component instance. - */ - protected function createTestMailComponent() - { - $component = new Mailer(); - $component->viewPath = $this->getTestFilePath(); - return $component; - } - - /** - * @return Mailer mailer instance - */ - protected function getTestMailComponent() - { - return Yii::$app->getComponent('mail'); - } - - // Tests : - - public function testSetupView() - { - $mailer = new Mailer(); - - $view = new View(); - $mailer->setView($view); - $this->assertEquals($view, $mailer->getView(), 'Unable to setup view!'); - - $viewConfig = [ - 'params' => [ - 'param1' => 'value1', - 'param2' => 'value2', - ] - ]; - $mailer->setView($viewConfig); - $view = $mailer->getView(); - $this->assertTrue(is_object($view), 'Unable to setup view via config!'); - $this->assertEquals($viewConfig['params'], $view->params, 'Unable to configure view via config array!'); - } - - /** - * @depends testSetupView - */ - public function testGetDefaultView() - { - $mailer = new Mailer(); - $view = $mailer->getView(); - $this->assertTrue(is_object($view), 'Unable to get default view!'); - } - - public function testCreateMessage() - { - $mailer = new Mailer(); - $message = $mailer->compose(); - $this->assertTrue(is_object($message), 'Unable to create message instance!'); - $this->assertEquals($mailer->messageClass, get_class($message), 'Invalid message class!'); - } - - /** - * @depends testCreateMessage - */ - public function testDefaultMessageConfig() - { - $mailer = new Mailer(); - - $notPropertyConfig = [ - 'charset' => 'utf-16', - 'from' => 'from@domain.com', - 'to' => 'to@domain.com', - 'cc' => 'cc@domain.com', - 'bcc' => 'bcc@domain.com', - 'subject' => 'Test subject', - 'textBody' => 'Test text body', - 'htmlBody' => 'Test HTML body', - ]; - $propertyConfig = [ - 'id' => 'test-id', - 'encoding' => 'test-encoding', - ]; - $messageConfig = array_merge($notPropertyConfig, $propertyConfig); - $mailer->messageConfig = $messageConfig; - - $message = $mailer->compose(); - - foreach ($notPropertyConfig as $name => $value) { - $this->assertEquals($value, $message->{'_' . $name}); - } - foreach ($propertyConfig as $name => $value) { - $this->assertEquals($value, $message->$name); - } - } - - /** - * @depends testGetDefaultView - */ - public function testRender() - { - $mailer = $this->getTestMailComponent(); - - $viewName = 'test_view'; - $viewFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $viewName . '.php'; - $viewFileContent = ''; - file_put_contents($viewFileName, $viewFileContent); - - $params = [ - 'testParam' => 'test output' - ]; - $renderResult = $mailer->render($viewName, $params); - $this->assertEquals($params['testParam'], $renderResult); - } - - /** - * @depends testRender - */ - public function testRenderLayout() - { - $mailer = $this->getTestMailComponent(); - - $filePath = $this->getTestFilePath(); - - $viewName = 'test_view2'; - $viewFileName = $filePath . DIRECTORY_SEPARATOR . $viewName . '.php'; - $viewFileContent = 'view file content'; - file_put_contents($viewFileName, $viewFileContent); - - $layoutName = 'test_layout'; - $layoutFileName = $filePath . DIRECTORY_SEPARATOR . $layoutName . '.php'; - $layoutFileContent = 'Begin Layout End Layout'; - file_put_contents($layoutFileName, $layoutFileContent); - - $renderResult = $mailer->render($viewName, [], $layoutName); - $this->assertEquals('Begin Layout ' . $viewFileContent . ' End Layout', $renderResult); - } - - /** - * @depends testCreateMessage - * @depends testRender - */ - public function testCompose() - { - $mailer = $this->getTestMailComponent(); - $mailer->htmlLayout = false; - $mailer->textLayout = false; - - $htmlViewName = 'test_html_view'; - $htmlViewFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $htmlViewName . '.php'; - $htmlViewFileContent = 'HTML view file content'; - file_put_contents($htmlViewFileName, $htmlViewFileContent); - - $textViewName = 'test_text_view'; - $textViewFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $textViewName . '.php'; - $textViewFileContent = 'Plain text view file content'; - file_put_contents($textViewFileName, $textViewFileContent); - - $message = $mailer->compose([ - 'html' => $htmlViewName, - 'text' => $textViewName, - ]); - $this->assertEquals($htmlViewFileContent, $message->_htmlBody, 'Unable to render html!'); - $this->assertEquals($textViewFileContent, $message->_textBody, 'Unable to render text!'); - - $message = $mailer->compose($htmlViewName); - $this->assertEquals($htmlViewFileContent, $message->_htmlBody, 'Unable to render html by direct view!'); - $this->assertEquals(strip_tags($htmlViewFileContent), $message->_textBody, 'Unable to render text by direct view!'); - } - - public function testUseFileTransport() - { - $mailer = new Mailer(); - $this->assertFalse($mailer->useFileTransport); - $this->assertEquals('@runtime/mail', $mailer->fileTransportPath); - - $mailer->fileTransportPath = '@yiiunit/runtime/mail'; - $mailer->useFileTransport = true; - $mailer->fileTransportCallback = function () { - return 'message.txt'; - }; - $message = $mailer->compose() - ->setTo('to@example.com') - ->setFrom('from@example.com') - ->setSubject('test subject') - ->setTextBody('text body' . microtime(true)); - $this->assertTrue($mailer->send($message)); - $file = Yii::getAlias($mailer->fileTransportPath) . '/message.txt'; - $this->assertTrue(is_file($file)); - $this->assertEquals($message->toString(), file_get_contents($file)); - } - - public function testBeforeSendEvent() - { - $message = new Message(); - - $mailerMock = $this->getMockBuilder('yiiunit\framework\mail\Mailer')->setMethods(['beforeSend', 'afterSend'])->getMock(); - $mailerMock->expects($this->once())->method('beforeSend')->with($message)->will($this->returnValue(true)); - $mailerMock->expects($this->once())->method('afterSend')->with($message, true); - $mailerMock->send($message); - } + public function setUp() + { + $this->mockApplication([ + 'components' => [ + 'mail' => $this->createTestMailComponent(), + ] + ]); + $filePath = $this->getTestFilePath(); + if (!file_exists($filePath)) { + FileHelper::createDirectory($filePath); + } + } + + public function tearDown() + { + $filePath = $this->getTestFilePath(); + if (file_exists($filePath)) { + FileHelper::removeDirectory($filePath); + } + } + + /** + * @return string test file path. + */ + protected function getTestFilePath() + { + return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid(); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestMailComponent() + { + $component = new Mailer(); + $component->viewPath = $this->getTestFilePath(); + + return $component; + } + + /** + * @return Mailer mailer instance + */ + protected function getTestMailComponent() + { + return Yii::$app->getComponent('mail'); + } + + // Tests : + + public function testSetupView() + { + $mailer = new Mailer(); + + $view = new View(); + $mailer->setView($view); + $this->assertEquals($view, $mailer->getView(), 'Unable to setup view!'); + + $viewConfig = [ + 'params' => [ + 'param1' => 'value1', + 'param2' => 'value2', + ] + ]; + $mailer->setView($viewConfig); + $view = $mailer->getView(); + $this->assertTrue(is_object($view), 'Unable to setup view via config!'); + $this->assertEquals($viewConfig['params'], $view->params, 'Unable to configure view via config array!'); + } + + /** + * @depends testSetupView + */ + public function testGetDefaultView() + { + $mailer = new Mailer(); + $view = $mailer->getView(); + $this->assertTrue(is_object($view), 'Unable to get default view!'); + } + + public function testCreateMessage() + { + $mailer = new Mailer(); + $message = $mailer->compose(); + $this->assertTrue(is_object($message), 'Unable to create message instance!'); + $this->assertEquals($mailer->messageClass, get_class($message), 'Invalid message class!'); + } + + /** + * @depends testCreateMessage + */ + public function testDefaultMessageConfig() + { + $mailer = new Mailer(); + + $notPropertyConfig = [ + 'charset' => 'utf-16', + 'from' => 'from@domain.com', + 'to' => 'to@domain.com', + 'cc' => 'cc@domain.com', + 'bcc' => 'bcc@domain.com', + 'subject' => 'Test subject', + 'textBody' => 'Test text body', + 'htmlBody' => 'Test HTML body', + ]; + $propertyConfig = [ + 'id' => 'test-id', + 'encoding' => 'test-encoding', + ]; + $messageConfig = array_merge($notPropertyConfig, $propertyConfig); + $mailer->messageConfig = $messageConfig; + + $message = $mailer->compose(); + + foreach ($notPropertyConfig as $name => $value) { + $this->assertEquals($value, $message->{'_' . $name}); + } + foreach ($propertyConfig as $name => $value) { + $this->assertEquals($value, $message->$name); + } + } + + /** + * @depends testGetDefaultView + */ + public function testRender() + { + $mailer = $this->getTestMailComponent(); + + $viewName = 'test_view'; + $viewFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $viewName . '.php'; + $viewFileContent = ''; + file_put_contents($viewFileName, $viewFileContent); + + $params = [ + 'testParam' => 'test output' + ]; + $renderResult = $mailer->render($viewName, $params); + $this->assertEquals($params['testParam'], $renderResult); + } + + /** + * @depends testRender + */ + public function testRenderLayout() + { + $mailer = $this->getTestMailComponent(); + + $filePath = $this->getTestFilePath(); + + $viewName = 'test_view2'; + $viewFileName = $filePath . DIRECTORY_SEPARATOR . $viewName . '.php'; + $viewFileContent = 'view file content'; + file_put_contents($viewFileName, $viewFileContent); + + $layoutName = 'test_layout'; + $layoutFileName = $filePath . DIRECTORY_SEPARATOR . $layoutName . '.php'; + $layoutFileContent = 'Begin Layout End Layout'; + file_put_contents($layoutFileName, $layoutFileContent); + + $renderResult = $mailer->render($viewName, [], $layoutName); + $this->assertEquals('Begin Layout ' . $viewFileContent . ' End Layout', $renderResult); + } + + /** + * @depends testCreateMessage + * @depends testRender + */ + public function testCompose() + { + $mailer = $this->getTestMailComponent(); + $mailer->htmlLayout = false; + $mailer->textLayout = false; + + $htmlViewName = 'test_html_view'; + $htmlViewFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $htmlViewName . '.php'; + $htmlViewFileContent = 'HTML view file content'; + file_put_contents($htmlViewFileName, $htmlViewFileContent); + + $textViewName = 'test_text_view'; + $textViewFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . $textViewName . '.php'; + $textViewFileContent = 'Plain text view file content'; + file_put_contents($textViewFileName, $textViewFileContent); + + $message = $mailer->compose([ + 'html' => $htmlViewName, + 'text' => $textViewName, + ]); + $this->assertEquals($htmlViewFileContent, $message->_htmlBody, 'Unable to render html!'); + $this->assertEquals($textViewFileContent, $message->_textBody, 'Unable to render text!'); + + $message = $mailer->compose($htmlViewName); + $this->assertEquals($htmlViewFileContent, $message->_htmlBody, 'Unable to render html by direct view!'); + $this->assertEquals(strip_tags($htmlViewFileContent), $message->_textBody, 'Unable to render text by direct view!'); + } + + public function testUseFileTransport() + { + $mailer = new Mailer(); + $this->assertFalse($mailer->useFileTransport); + $this->assertEquals('@runtime/mail', $mailer->fileTransportPath); + + $mailer->fileTransportPath = '@yiiunit/runtime/mail'; + $mailer->useFileTransport = true; + $mailer->fileTransportCallback = function () { + return 'message.txt'; + }; + $message = $mailer->compose() + ->setTo('to@example.com') + ->setFrom('from@example.com') + ->setSubject('test subject') + ->setTextBody('text body' . microtime(true)); + $this->assertTrue($mailer->send($message)); + $file = Yii::getAlias($mailer->fileTransportPath) . '/message.txt'; + $this->assertTrue(is_file($file)); + $this->assertEquals($message->toString(), file_get_contents($file)); + } + + public function testBeforeSendEvent() + { + $message = new Message(); + + $mailerMock = $this->getMockBuilder('yiiunit\framework\mail\Mailer')->setMethods(['beforeSend', 'afterSend'])->getMock(); + $mailerMock->expects($this->once())->method('beforeSend')->with($message)->will($this->returnValue(true)); + $mailerMock->expects($this->once())->method('afterSend')->with($message, true); + $mailerMock->send($message); + } } /** @@ -247,14 +248,15 @@ public function testBeforeSendEvent() */ class Mailer extends BaseMailer { - public $messageClass = 'yiiunit\framework\mail\Message'; - public $sentMessages = []; - - protected function sendMessage($message) - { - $this->sentMessages[] = $message; - return true; - } + public $messageClass = 'yiiunit\framework\mail\Message'; + public $sentMessages = []; + + protected function sendMessage($message) + { + $this->sentMessages[] = $message; + + return true; + } } /** @@ -262,117 +264,126 @@ protected function sendMessage($message) */ class Message extends BaseMessage { - public $id; - public $encoding; - public $_charset; - public $_from; - public $_replyTo; - public $_to; - public $_cc; - public $_bcc; - public $_subject; - public $_textBody; - public $_htmlBody; - - public function getCharset() - { - return $this->_charset; - } - - public function setCharset($charset) - { - $this->_charset = $charset; - return $this; - } - - public function getFrom() - { - return $this->_from; - } - - public function setFrom($from) - { - $this->_from = $from; - return $this; - } - - public function getTo() - { - return $this->_to; - } - - public function setTo($to) - { - $this->_to = $to; - return $this; - } - - public function getCc() - { - return $this->_cc; - } - - public function setCc($cc) - { - $this->_cc = $cc; - return $this; - } - - public function getBcc() - { - return $this->_bcc; - } - - public function setBcc($bcc) - { - $this->_bcc = $bcc; - return $this; - } - - public function getSubject() - { - return $this->_subject; - } - - public function setSubject($subject) - { - $this->_subject = $subject; - return $this; - } - - public function getReplyTo() - { - return $this->_replyTo; - } - - public function setReplyTo($replyTo) - { - $this->_replyTo = $replyTo; - return $this; - } - - public function setTextBody($text) - { - $this->_textBody = $text; - return $this; - } - - public function setHtmlBody($html) - { - $this->_htmlBody = $html; - return $this; - } - - public function attachContent($content, array $options = []) {} - - public function attach($fileName, array $options = []) {} - - public function embed($fileName, array $options = []) {} - - public function embedContent($content, array $options = []) {} - - public function toString() - { - return var_export($this, true); - } + public $id; + public $encoding; + public $_charset; + public $_from; + public $_replyTo; + public $_to; + public $_cc; + public $_bcc; + public $_subject; + public $_textBody; + public $_htmlBody; + + public function getCharset() + { + return $this->_charset; + } + + public function setCharset($charset) + { + $this->_charset = $charset; + + return $this; + } + + public function getFrom() + { + return $this->_from; + } + + public function setFrom($from) + { + $this->_from = $from; + + return $this; + } + + public function getTo() + { + return $this->_to; + } + + public function setTo($to) + { + $this->_to = $to; + + return $this; + } + + public function getCc() + { + return $this->_cc; + } + + public function setCc($cc) + { + $this->_cc = $cc; + + return $this; + } + + public function getBcc() + { + return $this->_bcc; + } + + public function setBcc($bcc) + { + $this->_bcc = $bcc; + + return $this; + } + + public function getSubject() + { + return $this->_subject; + } + + public function setSubject($subject) + { + $this->_subject = $subject; + + return $this; + } + + public function getReplyTo() + { + return $this->_replyTo; + } + + public function setReplyTo($replyTo) + { + $this->_replyTo = $replyTo; + + return $this; + } + + public function setTextBody($text) + { + $this->_textBody = $text; + + return $this; + } + + public function setHtmlBody($html) + { + $this->_htmlBody = $html; + + return $this; + } + + public function attachContent($content, array $options = []) {} + + public function attach($fileName, array $options = []) {} + + public function embed($fileName, array $options = []) {} + + public function embedContent($content, array $options = []) {} + + public function toString() + { + return var_export($this, true); + } } diff --git a/tests/unit/framework/mail/BaseMessageTest.php b/tests/unit/framework/mail/BaseMessageTest.php index c69ffbfb526..b8a0063d2fe 100644 --- a/tests/unit/framework/mail/BaseMessageTest.php +++ b/tests/unit/framework/mail/BaseMessageTest.php @@ -12,48 +12,49 @@ */ class BaseMessageTest extends TestCase { - public function setUp() - { - $this->mockApplication([ - 'components' => [ - 'mail' => $this->createTestEmailComponent() - ] - ]); - } - - /** - * @return Mailer test email component instance. - */ - protected function createTestEmailComponent() - { - $component = new TestMailer(); - return $component; - } - - /** - * @return TestMailer mailer instance. - */ - protected function getMailer() - { - return Yii::$app->getComponent('mail'); - } - - // Tests : - - public function testSend() - { - $mailer = $this->getMailer(); - $message = $mailer->compose(); - $message->send($mailer); - $this->assertEquals($message, $mailer->sentMessages[0], 'Unable to send message!'); - } - - public function testToString() - { - $mailer = $this->getMailer(); - $message = $mailer->compose(); - $this->assertEquals($message->toString(), '' . $message); - } + public function setUp() + { + $this->mockApplication([ + 'components' => [ + 'mail' => $this->createTestEmailComponent() + ] + ]); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new TestMailer(); + + return $component; + } + + /** + * @return TestMailer mailer instance. + */ + protected function getMailer() + { + return Yii::$app->getComponent('mail'); + } + + // Tests : + + public function testSend() + { + $mailer = $this->getMailer(); + $message = $mailer->compose(); + $message->send($mailer); + $this->assertEquals($message, $mailer->sentMessages[0], 'Unable to send message!'); + } + + public function testToString() + { + $mailer = $this->getMailer(); + $message = $mailer->compose(); + $this->assertEquals($message->toString(), '' . $message); + } } /** @@ -61,13 +62,13 @@ public function testToString() */ class TestMailer extends BaseMailer { - public $messageClass = 'yiiunit\framework\mail\TestMessage'; - public $sentMessages = []; + public $messageClass = 'yiiunit\framework\mail\TestMessage'; + public $sentMessages = []; - protected function sendMessage($message) - { - $this->sentMessages[] = $message; - } + protected function sendMessage($message) + { + $this->sentMessages[] = $message; + } } /** @@ -75,78 +76,78 @@ protected function sendMessage($message) */ class TestMessage extends BaseMessage { - public $text; - public $html; + public $text; + public $html; - public function getCharset() - { - return ''; - } + public function getCharset() + { + return ''; + } - public function setCharset($charset) {} + public function setCharset($charset) {} - public function getFrom() - { - return ''; - } + public function getFrom() + { + return ''; + } - public function setFrom($from) {} + public function setFrom($from) {} - public function getReplyTo() - { - return ''; - } + public function getReplyTo() + { + return ''; + } - public function setReplyTo($replyTo) {} + public function setReplyTo($replyTo) {} - public function getTo() - { - return ''; - } + public function getTo() + { + return ''; + } - public function setTo($to) {} + public function setTo($to) {} - public function getCc() - { - return ''; - } + public function getCc() + { + return ''; + } - public function setCc($cc) {} + public function setCc($cc) {} - public function getBcc() - { - return ''; - } + public function getBcc() + { + return ''; + } - public function setBcc($bcc){} + public function setBcc($bcc) {} - public function getSubject() - { - return ''; - } + public function getSubject() + { + return ''; + } - public function setSubject($subject) {} + public function setSubject($subject) {} - public function setTextBody($text) - { - $this->text = $text; - } + public function setTextBody($text) + { + $this->text = $text; + } - public function setHtmlBody($html) - { - $this->html = $html; - } + public function setHtmlBody($html) + { + $this->html = $html; + } - public function attachContent($content, array $options = []) {} + public function attachContent($content, array $options = []) {} - public function attach($fileName, array $options = []) {} + public function attach($fileName, array $options = []) {} - public function embed($fileName, array $options = []) {} + public function embed($fileName, array $options = []) {} - public function embedContent($content, array $options = []) {} + public function embedContent($content, array $options = []) {} - public function toString() - { - return get_class($this); - } + public function toString() + { + return get_class($this); + } } diff --git a/tests/unit/framework/rbac/ManagerTestCase.php b/tests/unit/framework/rbac/ManagerTestCase.php index 51c195e96a2..41eb10e98d5 100644 --- a/tests/unit/framework/rbac/ManagerTestCase.php +++ b/tests/unit/framework/rbac/ManagerTestCase.php @@ -8,258 +8,258 @@ abstract class ManagerTestCase extends TestCase { - /** @var \yii\rbac\PhpManager|\yii\rbac\DbManager */ - protected $auth; - - public function testCreateItem() - { - $type = Item::TYPE_TASK; - $name = 'editUser'; - $description = 'edit a user'; - $bizRule = 'checkUserIdentity()'; - $data = [1, 2, 3]; - $item = $this->auth->createItem($name, $type, $description, $bizRule, $data); - $this->assertTrue($item instanceof Item); - $this->assertEquals($item->type, $type); - $this->assertEquals($item->name, $name); - $this->assertEquals($item->description, $description); - $this->assertEquals($item->bizRule, $bizRule); - $this->assertEquals($item->data, $data); - - // test shortcut - $name2 = 'createUser'; - $item2 = $this->auth->createRole($name2, $description, $bizRule, $data); - $this->assertEquals($item2->type, Item::TYPE_ROLE); - - // test adding an item with the same name - $this->setExpectedException('\yii\base\Exception'); - $this->auth->createItem($name, $type, $description, $bizRule, $data); - } - - public function testGetItem() - { - $this->assertTrue($this->auth->getItem('readPost') instanceof Item); - $this->assertTrue($this->auth->getItem('reader') instanceof Item); - $this->assertNull($this->auth->getItem('unknown')); - } - - public function testRemoveAuthItem() - { - $this->assertTrue($this->auth->getItem('updatePost') instanceof Item); - $this->assertTrue($this->auth->removeItem('updatePost')); - $this->assertNull($this->auth->getItem('updatePost')); - $this->assertFalse($this->auth->removeItem('updatePost')); - } - - public function testChangeItemName() - { - $item = $this->auth->getItem('readPost'); - $this->assertTrue($item instanceof Item); - $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); - $item->name = 'readPost2'; - $item->save(); - $this->assertNull($this->auth->getItem('readPost')); - $this->assertEquals($this->auth->getItem('readPost2'), $item); - $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); - $this->assertTrue($this->auth->hasItemChild('reader', 'readPost2')); - } - - public function testAddItemChild() - { - $this->auth->addItemChild('createPost', 'updatePost'); - - // test adding upper level item to lower one - $this->setExpectedException('\yii\base\Exception'); - $this->auth->addItemChild('readPost', 'reader'); - } - - public function testAddItemChild2() - { - // test adding inexistent items - $this->setExpectedException('\yii\base\Exception'); - $this->assertFalse($this->auth->addItemChild('createPost2', 'updatePost')); - } - - public function testRemoveItemChild() - { - $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); - $this->assertTrue($this->auth->removeItemChild('reader', 'readPost')); - $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); - $this->assertFalse($this->auth->removeItemChild('reader', 'readPost')); - } - - public function testGetItemChildren() - { - $this->assertEquals([], $this->auth->getItemChildren('readPost')); - $children = $this->auth->getItemChildren('author'); - $this->assertEquals(3, count($children)); - $this->assertTrue(reset($children) instanceof Item); - } - - public function testAssign() - { - $auth = $this->auth->assign('new user', 'createPost', 'rule', 'data'); - $this->assertTrue($auth instanceof Assignment); - $this->assertEquals($auth->userId, 'new user'); - $this->assertEquals($auth->itemName, 'createPost'); - $this->assertEquals($auth->bizRule, 'rule'); - $this->assertEquals($auth->data, 'data'); - - $this->setExpectedException('\yii\base\Exception'); - $this->auth->assign('new user', 'createPost2', 'rule', 'data'); - } - - public function testRevoke() - { - $this->assertTrue($this->auth->isAssigned('author B', 'author')); - $auth = $this->auth->getAssignment('author B', 'author'); - $this->assertTrue($auth instanceof Assignment); - $this->assertTrue($this->auth->revoke('author B', 'author')); - $this->assertFalse($this->auth->isAssigned('author B', 'author')); - $this->assertFalse($this->auth->revoke('author B', 'author')); - } - - public function testRevokeAll() - { - $this->assertTrue($this->auth->revokeAll('reader E')); - $this->assertFalse($this->auth->isAssigned('reader E', 'reader')); - } - - public function testGetAssignments() - { - $this->auth->assign('author B', 'deletePost'); - $auths = $this->auth->getAssignments('author B'); - $this->assertEquals(2, count($auths)); - $this->assertTrue(reset($auths) instanceof Assignment); - } - - public function testGetItems() - { - $this->assertEquals(count($this->auth->getRoles()), 4); - $this->assertEquals(count($this->auth->getOperations()), 4); - $this->assertEquals(count($this->auth->getTasks()), 1); - $this->assertEquals(count($this->auth->getItems()), 9); - - $this->assertEquals(count($this->auth->getItems('author B', null)), 1); - $this->assertEquals(count($this->auth->getItems('author C', null)), 0); - $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_ROLE)), 1); - $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_OPERATION)), 0); - } - - public function testClearAll() - { - $this->auth->clearAll(); - $this->assertEquals(count($this->auth->getRoles()), 0); - $this->assertEquals(count($this->auth->getOperations()), 0); - $this->assertEquals(count($this->auth->getTasks()), 0); - $this->assertEquals(count($this->auth->getItems()), 0); - $this->assertEquals(count($this->auth->getAssignments('author B')), 0); - } - - public function testClearAssignments() - { - $this->auth->clearAssignments(); - $this->assertEquals(count($this->auth->getAssignments('author B')), 0); - } - - public function testDetectLoop() - { - $this->setExpectedException('\yii\base\Exception'); - $this->auth->addItemChild('readPost', 'readPost'); - } - - public function testExecuteBizRule() - { - $this->assertTrue($this->auth->executeBizRule(null, [], null)); - $this->assertTrue($this->auth->executeBizRule('return 1 == true;', [], null)); - $this->assertTrue($this->auth->executeBizRule('return $params[0] == $params[1];', [1, '1'], null)); - if (!defined('HHVM_VERSION')) { // invalid code crashes on HHVM - $this->assertFalse($this->auth->executeBizRule('invalid;', [], null)); - } - } - - public function testCheckAccess() - { - $results = [ - 'reader A' => [ - 'createPost' => false, - 'readPost' => true, - 'updatePost' => false, - 'updateOwnPost' => false, - 'deletePost' => false, - ], - 'author B' => [ - 'createPost' => true, - 'readPost' => true, - 'updatePost' => true, - 'updateOwnPost' => true, - 'deletePost' => false, - ], - 'editor C' => [ - 'createPost' => false, - 'readPost' => true, - 'updatePost' => true, - 'updateOwnPost' => false, - 'deletePost' => false, - ], - 'admin D' => [ - 'createPost' => true, - 'readPost' => true, - 'updatePost' => true, - 'updateOwnPost' => false, - 'deletePost' => true, - ], - 'reader E' => [ - 'createPost' => false, - 'readPost' => false, - 'updatePost' => false, - 'updateOwnPost' => false, - 'deletePost' => false, - ], - ]; - - $params = ['authorID' => 'author B']; - - foreach (['reader A', 'author B', 'editor C', 'admin D'] as $user) { - $params['userID'] = $user; - foreach (['createPost', 'readPost', 'updatePost', 'updateOwnPost', 'deletePost'] as $operation) { - $result = $this->auth->checkAccess($user, $operation, $params); - $this->assertEquals($results[$user][$operation], $result); - } - } - } - - protected function prepareData() - { - $this->auth->createOperation('createPost', 'create a post'); - $this->auth->createOperation('readPost', 'read a post'); - $this->auth->createOperation('updatePost', 'update a post'); - $this->auth->createOperation('deletePost', 'delete a post'); - - $task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'return $params["authorID"] == $params["userID"];'); - $task->addChild('updatePost'); - - $role = $this->auth->createRole('reader'); - $role->addChild('readPost'); - - $role = $this->auth->createRole('author'); - $role->addChild('reader'); - $role->addChild('createPost'); - $role->addChild('updateOwnPost'); - - $role = $this->auth->createRole('editor'); - $role->addChild('reader'); - $role->addChild('updatePost'); - - $role = $this->auth->createRole('admin'); - $role->addChild('editor'); - $role->addChild('author'); - $role->addChild('deletePost'); - - $this->auth->assign('reader A', 'reader'); - $this->auth->assign('author B', 'author'); - $this->auth->assign('editor C', 'editor'); - $this->auth->assign('admin D', 'admin'); - $this->auth->assign('reader E', 'reader'); - } + /** @var \yii\rbac\PhpManager|\yii\rbac\DbManager */ + protected $auth; + + public function testCreateItem() + { + $type = Item::TYPE_TASK; + $name = 'editUser'; + $description = 'edit a user'; + $bizRule = 'checkUserIdentity()'; + $data = [1, 2, 3]; + $item = $this->auth->createItem($name, $type, $description, $bizRule, $data); + $this->assertTrue($item instanceof Item); + $this->assertEquals($item->type, $type); + $this->assertEquals($item->name, $name); + $this->assertEquals($item->description, $description); + $this->assertEquals($item->bizRule, $bizRule); + $this->assertEquals($item->data, $data); + + // test shortcut + $name2 = 'createUser'; + $item2 = $this->auth->createRole($name2, $description, $bizRule, $data); + $this->assertEquals($item2->type, Item::TYPE_ROLE); + + // test adding an item with the same name + $this->setExpectedException('\yii\base\Exception'); + $this->auth->createItem($name, $type, $description, $bizRule, $data); + } + + public function testGetItem() + { + $this->assertTrue($this->auth->getItem('readPost') instanceof Item); + $this->assertTrue($this->auth->getItem('reader') instanceof Item); + $this->assertNull($this->auth->getItem('unknown')); + } + + public function testRemoveAuthItem() + { + $this->assertTrue($this->auth->getItem('updatePost') instanceof Item); + $this->assertTrue($this->auth->removeItem('updatePost')); + $this->assertNull($this->auth->getItem('updatePost')); + $this->assertFalse($this->auth->removeItem('updatePost')); + } + + public function testChangeItemName() + { + $item = $this->auth->getItem('readPost'); + $this->assertTrue($item instanceof Item); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $item->name = 'readPost2'; + $item->save(); + $this->assertNull($this->auth->getItem('readPost')); + $this->assertEquals($this->auth->getItem('readPost2'), $item); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost2')); + } + + public function testAddItemChild() + { + $this->auth->addItemChild('createPost', 'updatePost'); + + // test adding upper level item to lower one + $this->setExpectedException('\yii\base\Exception'); + $this->auth->addItemChild('readPost', 'reader'); + } + + public function testAddItemChild2() + { + // test adding inexistent items + $this->setExpectedException('\yii\base\Exception'); + $this->assertFalse($this->auth->addItemChild('createPost2', 'updatePost')); + } + + public function testRemoveItemChild() + { + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->removeItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->removeItemChild('reader', 'readPost')); + } + + public function testGetItemChildren() + { + $this->assertEquals([], $this->auth->getItemChildren('readPost')); + $children = $this->auth->getItemChildren('author'); + $this->assertEquals(3, count($children)); + $this->assertTrue(reset($children) instanceof Item); + } + + public function testAssign() + { + $auth = $this->auth->assign('new user', 'createPost', 'rule', 'data'); + $this->assertTrue($auth instanceof Assignment); + $this->assertEquals($auth->userId, 'new user'); + $this->assertEquals($auth->itemName, 'createPost'); + $this->assertEquals($auth->bizRule, 'rule'); + $this->assertEquals($auth->data, 'data'); + + $this->setExpectedException('\yii\base\Exception'); + $this->auth->assign('new user', 'createPost2', 'rule', 'data'); + } + + public function testRevoke() + { + $this->assertTrue($this->auth->isAssigned('author B', 'author')); + $auth = $this->auth->getAssignment('author B', 'author'); + $this->assertTrue($auth instanceof Assignment); + $this->assertTrue($this->auth->revoke('author B', 'author')); + $this->assertFalse($this->auth->isAssigned('author B', 'author')); + $this->assertFalse($this->auth->revoke('author B', 'author')); + } + + public function testRevokeAll() + { + $this->assertTrue($this->auth->revokeAll('reader E')); + $this->assertFalse($this->auth->isAssigned('reader E', 'reader')); + } + + public function testGetAssignments() + { + $this->auth->assign('author B', 'deletePost'); + $auths = $this->auth->getAssignments('author B'); + $this->assertEquals(2, count($auths)); + $this->assertTrue(reset($auths) instanceof Assignment); + } + + public function testGetItems() + { + $this->assertEquals(count($this->auth->getRoles()), 4); + $this->assertEquals(count($this->auth->getOperations()), 4); + $this->assertEquals(count($this->auth->getTasks()), 1); + $this->assertEquals(count($this->auth->getItems()), 9); + + $this->assertEquals(count($this->auth->getItems('author B', null)), 1); + $this->assertEquals(count($this->auth->getItems('author C', null)), 0); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_ROLE)), 1); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_OPERATION)), 0); + } + + public function testClearAll() + { + $this->auth->clearAll(); + $this->assertEquals(count($this->auth->getRoles()), 0); + $this->assertEquals(count($this->auth->getOperations()), 0); + $this->assertEquals(count($this->auth->getTasks()), 0); + $this->assertEquals(count($this->auth->getItems()), 0); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testClearAssignments() + { + $this->auth->clearAssignments(); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testDetectLoop() + { + $this->setExpectedException('\yii\base\Exception'); + $this->auth->addItemChild('readPost', 'readPost'); + } + + public function testExecuteBizRule() + { + $this->assertTrue($this->auth->executeBizRule(null, [], null)); + $this->assertTrue($this->auth->executeBizRule('return 1 == true;', [], null)); + $this->assertTrue($this->auth->executeBizRule('return $params[0] == $params[1];', [1, '1'], null)); + if (!defined('HHVM_VERSION')) { // invalid code crashes on HHVM + $this->assertFalse($this->auth->executeBizRule('invalid;', [], null)); + } + } + + public function testCheckAccess() + { + $results = [ + 'reader A' => [ + 'createPost' => false, + 'readPost' => true, + 'updatePost' => false, + 'updateOwnPost' => false, + 'deletePost' => false, + ], + 'author B' => [ + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => true, + 'deletePost' => false, + ], + 'editor C' => [ + 'createPost' => false, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => false, + ], + 'admin D' => [ + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => true, + ], + 'reader E' => [ + 'createPost' => false, + 'readPost' => false, + 'updatePost' => false, + 'updateOwnPost' => false, + 'deletePost' => false, + ], + ]; + + $params = ['authorID' => 'author B']; + + foreach (['reader A', 'author B', 'editor C', 'admin D'] as $user) { + $params['userID'] = $user; + foreach (['createPost', 'readPost', 'updatePost', 'updateOwnPost', 'deletePost'] as $operation) { + $result = $this->auth->checkAccess($user, $operation, $params); + $this->assertEquals($results[$user][$operation], $result); + } + } + } + + protected function prepareData() + { + $this->auth->createOperation('createPost', 'create a post'); + $this->auth->createOperation('readPost', 'read a post'); + $this->auth->createOperation('updatePost', 'update a post'); + $this->auth->createOperation('deletePost', 'delete a post'); + + $task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'return $params["authorID"] == $params["userID"];'); + $task->addChild('updatePost'); + + $role = $this->auth->createRole('reader'); + $role->addChild('readPost'); + + $role = $this->auth->createRole('author'); + $role->addChild('reader'); + $role->addChild('createPost'); + $role->addChild('updateOwnPost'); + + $role = $this->auth->createRole('editor'); + $role->addChild('reader'); + $role->addChild('updatePost'); + + $role = $this->auth->createRole('admin'); + $role->addChild('editor'); + $role->addChild('author'); + $role->addChild('deletePost'); + + $this->auth->assign('reader A', 'reader'); + $this->auth->assign('author B', 'author'); + $this->auth->assign('editor C', 'editor'); + $this->auth->assign('admin D', 'admin'); + $this->auth->assign('reader E', 'reader'); + } } diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php index 8c5d366f0f8..7e5927a4974 100644 --- a/tests/unit/framework/rbac/PhpManagerTest.php +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -10,29 +10,29 @@ */ class PhpManagerTest extends ManagerTestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $authFile = Yii::$app->getRuntimePath() . '/rbac.php'; - @unlink($authFile); - $this->auth = new PhpManager; - $this->auth->authFile = $authFile; - $this->auth->init(); - $this->prepareData(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $authFile = Yii::$app->getRuntimePath() . '/rbac.php'; + @unlink($authFile); + $this->auth = new PhpManager; + $this->auth->authFile = $authFile; + $this->auth->init(); + $this->prepareData(); + } - protected function tearDown() - { - parent::tearDown(); - @unlink($this->auth->authFile); - } + protected function tearDown() + { + parent::tearDown(); + @unlink($this->auth->authFile); + } - public function testSaveLoad() - { - $this->auth->save(); - $this->auth->clearAll(); - $this->auth->load(); - $this->testCheckAccess(); - } + public function testSaveLoad() + { + $this->auth->save(); + $this->auth->clearAll(); + $this->auth->load(); + $this->testCheckAccess(); + } } diff --git a/tests/unit/framework/requirements/YiiRequirementCheckerTest.php b/tests/unit/framework/requirements/YiiRequirementCheckerTest.php index 990fa3cfd59..c31e3d1467d 100644 --- a/tests/unit/framework/requirements/YiiRequirementCheckerTest.php +++ b/tests/unit/framework/requirements/YiiRequirementCheckerTest.php @@ -11,187 +11,187 @@ */ class YiiRequirementCheckerTest extends TestCase { - public function testCheck() - { - $requirementsChecker = new YiiRequirementChecker(); - - $requirements = [ - 'requirementPass' => [ - 'name' => 'Requirement 1', - 'mandatory' => true, - 'condition' => true, - 'by' => 'Requirement 1', - 'memo' => 'Requirement 1', - ], - 'requirementError' => [ - 'name' => 'Requirement 2', - 'mandatory' => true, - 'condition' => false, - 'by' => 'Requirement 2', - 'memo' => 'Requirement 2', - ], - 'requirementWarning' => [ - 'name' => 'Requirement 3', - 'mandatory' => false, - 'condition' => false, - 'by' => 'Requirement 3', - 'memo' => 'Requirement 3', - ], - ]; - - $checkResult = $requirementsChecker->check($requirements)->getResult(); - $summary = $checkResult['summary']; - - $this->assertEquals(count($requirements), $summary['total'], 'Wrong summary total!'); - $this->assertEquals(1, $summary['errors'], 'Wrong summary errors!'); - $this->assertEquals(1, $summary['warnings'], 'Wrong summary warnings!'); - - $checkedRequirements = $checkResult['requirements']; - $requirementsKeys = array_flip(array_keys($requirements)); - - $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['error'], 'Passed requirement has an error!'); - $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['warning'], 'Passed requirement has a warning!'); - - $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementError']]['error'], 'Error requirement has no error!'); - - $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementWarning']]['error'], 'Error requirement has an error!'); - $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementWarning']]['warning'], 'Error requirement has no warning!'); - } - - /** - * @depends testCheck - */ - public function testCheckEval() - { - $requirementsChecker = new YiiRequirementChecker(); - - $requirements = [ - 'requirementPass' => [ - 'name' => 'Requirement 1', - 'mandatory' => true, - 'condition' => 'eval:2>1', - 'by' => 'Requirement 1', - 'memo' => 'Requirement 1', - ], - 'requirementError' => [ - 'name' => 'Requirement 2', - 'mandatory' => true, - 'condition' => 'eval:2<1', - 'by' => 'Requirement 2', - 'memo' => 'Requirement 2', - ], - ]; - - $checkResult = $requirementsChecker->check($requirements)->getResult(); - $checkedRequirements = $checkResult['requirements']; - $requirementsKeys = array_flip(array_keys($requirements)); - - $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['error'], 'Passed requirement has an error!'); - $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['warning'], 'Passed requirement has a warning!'); - - $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementError']]['error'], 'Error requirement has no error!'); - } - - /** - * @depends testCheck - */ - public function testCheckChained() - { - $requirementsChecker = new YiiRequirementChecker(); - - $requirements1 = [ - [ - 'name' => 'Requirement 1', - 'mandatory' => true, - 'condition' => true, - 'by' => 'Requirement 1', - 'memo' => 'Requirement 1', - ], - ]; - $requirements2 = [ - [ - 'name' => 'Requirement 2', - 'mandatory' => true, - 'condition' => true, - 'by' => 'Requirement 2', - 'memo' => 'Requirement 2', - ], - ]; - $checkResult = $requirementsChecker->check($requirements1)->check($requirements2)->getResult(); - - $mergedRequirements = array_merge($requirements1, $requirements2); - - $this->assertEquals(count($mergedRequirements), $checkResult['summary']['total'], 'Wrong total checks count!'); - foreach ($mergedRequirements as $key => $mergedRequirement) { - $this->assertEquals($mergedRequirement['name'], $checkResult['requirements'][$key]['name'], 'Wrong requirements list!'); - } - } - - public function testCheckPhpExtensionVersion() - { - $requirementsChecker = new YiiRequirementChecker(); - - $this->assertFalse($requirementsChecker->checkPhpExtensionVersion('some_unexisting_php_extension', '0.1'), 'No fail while checking unexisting extension!'); - $this->assertTrue($requirementsChecker->checkPhpExtensionVersion('pdo', '1.0'), 'Unable to check PDO version!'); - } - - /** - * Data provider for [[testGetByteSize()]]. - * @return array - */ - public function dataProviderGetByteSize() - { - return [ - ['456', 456], - ['5K', 5*1024], - ['16KB', 16*1024], - ['4M', 4*1024*1024], - ['14MB', 14*1024*1024], - ['7G', 7*1024*1024*1024], - ['12GB', 12*1024*1024*1024], - ]; - } - - /** - * @dataProvider dataProviderGetByteSize - * - * @param string $verboseValue verbose value. - * @param integer $expectedByteSize expected byte size. - */ - public function testGetByteSize($verboseValue, $expectedByteSize) - { - $requirementsChecker = new YiiRequirementChecker(); - - $this->assertEquals($expectedByteSize, $requirementsChecker->getByteSize($verboseValue), "Wrong byte size for '{$verboseValue}'!"); - } - - /** - * Data provider for [[testCompareByteSize()]] - * @return array - */ - public function dataProviderCompareByteSize() - { - return [ - ['2M', '2K', '>', true], - ['2M', '2K', '>=', true], - ['1K', '1024', '==', true], - ['10M', '11M', '<', true], - ['10M', '11M', '<=', true], - ]; - } - - /** - * @depends testGetByteSize - * @dataProvider dataProviderCompareByteSize - * - * @param string $a first value. - * @param string $b second value. - * @param string $compare comparison. - * @param boolean $expectedComparisonResult expected comparison result. - */ - public function testCompareByteSize($a, $b, $compare, $expectedComparisonResult) - { - $requirementsChecker = new YiiRequirementChecker(); - $this->assertEquals($expectedComparisonResult, $requirementsChecker->compareByteSize($a, $b, $compare), "Wrong compare '{$a}{$compare}{$b}'"); - } + public function testCheck() + { + $requirementsChecker = new YiiRequirementChecker(); + + $requirements = [ + 'requirementPass' => [ + 'name' => 'Requirement 1', + 'mandatory' => true, + 'condition' => true, + 'by' => 'Requirement 1', + 'memo' => 'Requirement 1', + ], + 'requirementError' => [ + 'name' => 'Requirement 2', + 'mandatory' => true, + 'condition' => false, + 'by' => 'Requirement 2', + 'memo' => 'Requirement 2', + ], + 'requirementWarning' => [ + 'name' => 'Requirement 3', + 'mandatory' => false, + 'condition' => false, + 'by' => 'Requirement 3', + 'memo' => 'Requirement 3', + ], + ]; + + $checkResult = $requirementsChecker->check($requirements)->getResult(); + $summary = $checkResult['summary']; + + $this->assertEquals(count($requirements), $summary['total'], 'Wrong summary total!'); + $this->assertEquals(1, $summary['errors'], 'Wrong summary errors!'); + $this->assertEquals(1, $summary['warnings'], 'Wrong summary warnings!'); + + $checkedRequirements = $checkResult['requirements']; + $requirementsKeys = array_flip(array_keys($requirements)); + + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['error'], 'Passed requirement has an error!'); + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['warning'], 'Passed requirement has a warning!'); + + $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementError']]['error'], 'Error requirement has no error!'); + + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementWarning']]['error'], 'Error requirement has an error!'); + $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementWarning']]['warning'], 'Error requirement has no warning!'); + } + + /** + * @depends testCheck + */ + public function testCheckEval() + { + $requirementsChecker = new YiiRequirementChecker(); + + $requirements = [ + 'requirementPass' => [ + 'name' => 'Requirement 1', + 'mandatory' => true, + 'condition' => 'eval:2>1', + 'by' => 'Requirement 1', + 'memo' => 'Requirement 1', + ], + 'requirementError' => [ + 'name' => 'Requirement 2', + 'mandatory' => true, + 'condition' => 'eval:2<1', + 'by' => 'Requirement 2', + 'memo' => 'Requirement 2', + ], + ]; + + $checkResult = $requirementsChecker->check($requirements)->getResult(); + $checkedRequirements = $checkResult['requirements']; + $requirementsKeys = array_flip(array_keys($requirements)); + + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['error'], 'Passed requirement has an error!'); + $this->assertEquals(false, $checkedRequirements[$requirementsKeys['requirementPass']]['warning'], 'Passed requirement has a warning!'); + + $this->assertEquals(true, $checkedRequirements[$requirementsKeys['requirementError']]['error'], 'Error requirement has no error!'); + } + + /** + * @depends testCheck + */ + public function testCheckChained() + { + $requirementsChecker = new YiiRequirementChecker(); + + $requirements1 = [ + [ + 'name' => 'Requirement 1', + 'mandatory' => true, + 'condition' => true, + 'by' => 'Requirement 1', + 'memo' => 'Requirement 1', + ], + ]; + $requirements2 = [ + [ + 'name' => 'Requirement 2', + 'mandatory' => true, + 'condition' => true, + 'by' => 'Requirement 2', + 'memo' => 'Requirement 2', + ], + ]; + $checkResult = $requirementsChecker->check($requirements1)->check($requirements2)->getResult(); + + $mergedRequirements = array_merge($requirements1, $requirements2); + + $this->assertEquals(count($mergedRequirements), $checkResult['summary']['total'], 'Wrong total checks count!'); + foreach ($mergedRequirements as $key => $mergedRequirement) { + $this->assertEquals($mergedRequirement['name'], $checkResult['requirements'][$key]['name'], 'Wrong requirements list!'); + } + } + + public function testCheckPhpExtensionVersion() + { + $requirementsChecker = new YiiRequirementChecker(); + + $this->assertFalse($requirementsChecker->checkPhpExtensionVersion('some_unexisting_php_extension', '0.1'), 'No fail while checking unexisting extension!'); + $this->assertTrue($requirementsChecker->checkPhpExtensionVersion('pdo', '1.0'), 'Unable to check PDO version!'); + } + + /** + * Data provider for [[testGetByteSize()]]. + * @return array + */ + public function dataProviderGetByteSize() + { + return [ + ['456', 456], + ['5K', 5*1024], + ['16KB', 16*1024], + ['4M', 4*1024*1024], + ['14MB', 14*1024*1024], + ['7G', 7*1024*1024*1024], + ['12GB', 12*1024*1024*1024], + ]; + } + + /** + * @dataProvider dataProviderGetByteSize + * + * @param string $verboseValue verbose value. + * @param integer $expectedByteSize expected byte size. + */ + public function testGetByteSize($verboseValue, $expectedByteSize) + { + $requirementsChecker = new YiiRequirementChecker(); + + $this->assertEquals($expectedByteSize, $requirementsChecker->getByteSize($verboseValue), "Wrong byte size for '{$verboseValue}'!"); + } + + /** + * Data provider for [[testCompareByteSize()]] + * @return array + */ + public function dataProviderCompareByteSize() + { + return [ + ['2M', '2K', '>', true], + ['2M', '2K', '>=', true], + ['1K', '1024', '==', true], + ['10M', '11M', '<', true], + ['10M', '11M', '<=', true], + ]; + } + + /** + * @depends testGetByteSize + * @dataProvider dataProviderCompareByteSize + * + * @param string $a first value. + * @param string $b second value. + * @param string $compare comparison. + * @param boolean $expectedComparisonResult expected comparison result. + */ + public function testCompareByteSize($a, $b, $compare, $expectedComparisonResult) + { + $requirementsChecker = new YiiRequirementChecker(); + $this->assertEquals($expectedComparisonResult, $requirementsChecker->compareByteSize($a, $b, $compare), "Wrong compare '{$a}{$compare}{$b}'"); + } } diff --git a/tests/unit/framework/test/ActiveFixtureTest.php b/tests/unit/framework/test/ActiveFixtureTest.php index a5ba139b2d7..6aead47f19a 100644 --- a/tests/unit/framework/test/ActiveFixtureTest.php +++ b/tests/unit/framework/test/ActiveFixtureTest.php @@ -16,36 +16,36 @@ class CustomerFixture extends ActiveFixture { - public $modelClass = 'yiiunit\data\ar\Customer'; + public $modelClass = 'yiiunit\data\ar\Customer'; } class MyDbTestCase { - use FixtureTrait; + use FixtureTrait; - public function setUp() - { - $this->unloadFixtures(); - $this->loadFixtures(); - } + public function setUp() + { + $this->unloadFixtures(); + $this->loadFixtures(); + } - public function tearDown() - { - } + public function tearDown() + { + } - public function fixtures() - { - return [ - 'customers' => CustomerFixture::className(), - ]; - } + public function fixtures() + { + return [ + 'customers' => CustomerFixture::className(), + ]; + } - public function globalFixtures() - { - return [ - InitDbFixture::className(), - ]; - } + public function globalFixtures() + { + return [ + InitDbFixture::className(), + ]; + } } /** @@ -55,42 +55,42 @@ public function globalFixtures() */ class ActiveFixtureTest extends DatabaseTestCase { - public function setUp() - { - parent::setUp(); - \Yii::$app->setComponent('db', $this->getConnection()); - ActiveRecord::$db = $this->getConnection(); - } + public function setUp() + { + parent::setUp(); + \Yii::$app->setComponent('db', $this->getConnection()); + ActiveRecord::$db = $this->getConnection(); + } - public function tearDown() - { - parent::tearDown(); - } + public function tearDown() + { + parent::tearDown(); + } - public function testGetData() - { - $test = new MyDbTestCase(); - $test->setUp(); - $fixture = $test->getFixture('customers'); - $this->assertEquals(CustomerFixture::className(), get_class($fixture)); - $this->assertEquals(2, count($fixture)); - $this->assertEquals(1, $fixture['customer1']['id']); - $this->assertEquals('customer1@example.com', $fixture['customer1']['email']); - $this->assertEquals(2, $fixture['customer2']['id']); - $this->assertEquals('customer2@example.com', $fixture['customer2']['email']); - $test->tearDown(); - } + public function testGetData() + { + $test = new MyDbTestCase(); + $test->setUp(); + $fixture = $test->getFixture('customers'); + $this->assertEquals(CustomerFixture::className(), get_class($fixture)); + $this->assertEquals(2, count($fixture)); + $this->assertEquals(1, $fixture['customer1']['id']); + $this->assertEquals('customer1@example.com', $fixture['customer1']['email']); + $this->assertEquals(2, $fixture['customer2']['id']); + $this->assertEquals('customer2@example.com', $fixture['customer2']['email']); + $test->tearDown(); + } - public function testGetModel() - { - $test = new MyDbTestCase(); - $test->setUp(); - $fixture = $test->getFixture('customers'); - $this->assertEquals(Customer::className(), get_class($fixture->getModel('customer1'))); - $this->assertEquals(1, $fixture->getModel('customer1')->id); - $this->assertEquals('customer1@example.com', $fixture->getModel('customer1')->email); - $this->assertEquals(2, $fixture->getModel('customer2')->id); - $this->assertEquals('customer2@example.com', $fixture->getModel('customer2')->email); - $test->tearDown(); - } + public function testGetModel() + { + $test = new MyDbTestCase(); + $test->setUp(); + $fixture = $test->getFixture('customers'); + $this->assertEquals(Customer::className(), get_class($fixture->getModel('customer1'))); + $this->assertEquals(1, $fixture->getModel('customer1')->id); + $this->assertEquals('customer1@example.com', $fixture->getModel('customer1')->email); + $this->assertEquals(2, $fixture->getModel('customer2')->id); + $this->assertEquals('customer2@example.com', $fixture->getModel('customer2')->email); + $test->tearDown(); + } } diff --git a/tests/unit/framework/test/FixtureTest.php b/tests/unit/framework/test/FixtureTest.php index 8ce09ae51ff..9284d99243b 100644 --- a/tests/unit/framework/test/FixtureTest.php +++ b/tests/unit/framework/test/FixtureTest.php @@ -13,159 +13,157 @@ class Fixture1 extends Fixture { - public $depends = ['yiiunit\framework\test\Fixture2']; + public $depends = ['yiiunit\framework\test\Fixture2']; - public function load() - { - MyTestCase::$load .= '1'; - } + public function load() + { + MyTestCase::$load .= '1'; + } - public function unload() - { - MyTestCase::$unload .= '1'; - } + public function unload() + { + MyTestCase::$unload .= '1'; + } } class Fixture2 extends Fixture { - public $depends = ['yiiunit\framework\test\Fixture3']; - public function load() - { - MyTestCase::$load .= '2'; - } - - - public function unload() - { - MyTestCase::$unload .= '2'; - } + public $depends = ['yiiunit\framework\test\Fixture3']; + public function load() + { + MyTestCase::$load .= '2'; + } + + public function unload() + { + MyTestCase::$unload .= '2'; + } } class Fixture3 extends Fixture { - public function load() - { - MyTestCase::$load .= '3'; - } - - - public function unload() - { - MyTestCase::$unload .= '3'; - } + public function load() + { + MyTestCase::$load .= '3'; + } + + public function unload() + { + MyTestCase::$unload .= '3'; + } } class MyTestCase { - use FixtureTrait; - - public $scenario = 1; - public static $load; - public static $unload; - - public function setUp() - { - $this->loadFixtures(); - } - - public function tearDown() - { - $this->unloadFixtures(); - } - - public function fetchFixture($name) - { - return $this->getFixture($name); - } - - public function fixtures() - { - switch ($this->scenario) { - case 0: return []; - case 1: return [ - 'fixture1' => Fixture1::className(), - ]; - case 2: return [ - 'fixture2' => Fixture2::className(), - ]; - case 3: return [ - 'fixture3' => Fixture3::className(), - ]; - case 4: return [ - 'fixture1' => Fixture1::className(), - 'fixture2' => Fixture2::className(), - ]; - case 5: return [ - 'fixture2' => Fixture2::className(), - 'fixture3' => Fixture3::className(), - ]; - case 6: return [ - 'fixture1' => Fixture1::className(), - 'fixture3' => Fixture3::className(), - ]; - case 7: - default: return [ - 'fixture1' => Fixture1::className(), - 'fixture2' => Fixture2::className(), - 'fixture3' => Fixture3::className(), - ]; - } - } + use FixtureTrait; + + public $scenario = 1; + public static $load; + public static $unload; + + public function setUp() + { + $this->loadFixtures(); + } + + public function tearDown() + { + $this->unloadFixtures(); + } + + public function fetchFixture($name) + { + return $this->getFixture($name); + } + + public function fixtures() + { + switch ($this->scenario) { + case 0: return []; + case 1: return [ + 'fixture1' => Fixture1::className(), + ]; + case 2: return [ + 'fixture2' => Fixture2::className(), + ]; + case 3: return [ + 'fixture3' => Fixture3::className(), + ]; + case 4: return [ + 'fixture1' => Fixture1::className(), + 'fixture2' => Fixture2::className(), + ]; + case 5: return [ + 'fixture2' => Fixture2::className(), + 'fixture3' => Fixture3::className(), + ]; + case 6: return [ + 'fixture1' => Fixture1::className(), + 'fixture3' => Fixture3::className(), + ]; + case 7: + default: return [ + 'fixture1' => Fixture1::className(), + 'fixture2' => Fixture2::className(), + 'fixture3' => Fixture3::className(), + ]; + } + } } class FixtureTest extends TestCase { - public function testDependencies() - { - foreach ($this->getDependencyTests() as $scenario => $result) { - $test = new MyTestCase(); - $test->scenario = $scenario; - $test->setUp(); - foreach ($result as $name => $loaded) { - $this->assertEquals($loaded, $test->fetchFixture($name) !== null, "Verifying scenario $scenario fixture $name"); - } - } - } - - public function testLoadSequence() - { - foreach ($this->getLoadSequenceTests() as $scenario => $result) { - $test = new MyTestCase(); - $test->scenario = $scenario; - MyTestCase::$load = ''; - MyTestCase::$unload = ''; - $test->setUp(); - $this->assertEquals($result[0], MyTestCase::$load, "Verifying scenario $scenario load sequence"); - $test->tearDown(); - $this->assertEquals($result[1], MyTestCase::$unload, "Verifying scenario $scenario unload sequence"); - } - } - - protected function getDependencyTests() - { - return [ - 0 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false], - 1 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => false], - 2 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => false], - 3 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => true], - 4 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => false], - 5 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => true], - 6 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => true], - 7 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => true], - ]; - } - - protected function getLoadSequenceTests() - { - return [ - 0 => ['', ''], - 1 => ['321', '123'], - 2 => ['32', '23'], - 3 => ['3', '3'], - 4 => ['321', '123'], - 5 => ['32', '23'], - 6 => ['321', '123'], - 7 => ['321', '123'], - ]; - } + public function testDependencies() + { + foreach ($this->getDependencyTests() as $scenario => $result) { + $test = new MyTestCase(); + $test->scenario = $scenario; + $test->setUp(); + foreach ($result as $name => $loaded) { + $this->assertEquals($loaded, $test->fetchFixture($name) !== null, "Verifying scenario $scenario fixture $name"); + } + } + } + + public function testLoadSequence() + { + foreach ($this->getLoadSequenceTests() as $scenario => $result) { + $test = new MyTestCase(); + $test->scenario = $scenario; + MyTestCase::$load = ''; + MyTestCase::$unload = ''; + $test->setUp(); + $this->assertEquals($result[0], MyTestCase::$load, "Verifying scenario $scenario load sequence"); + $test->tearDown(); + $this->assertEquals($result[1], MyTestCase::$unload, "Verifying scenario $scenario unload sequence"); + } + } + + protected function getDependencyTests() + { + return [ + 0 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => false], + 1 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => false], + 2 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => false], + 3 => ['fixture1' => false, 'fixture2' => false, 'fixture3' => true], + 4 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => false], + 5 => ['fixture1' => false, 'fixture2' => true, 'fixture3' => true], + 6 => ['fixture1' => true, 'fixture2' => false, 'fixture3' => true], + 7 => ['fixture1' => true, 'fixture2' => true, 'fixture3' => true], + ]; + } + + protected function getLoadSequenceTests() + { + return [ + 0 => ['', ''], + 1 => ['321', '123'], + 2 => ['32', '23'], + 3 => ['3', '3'], + 4 => ['321', '123'], + 5 => ['32', '23'], + 6 => ['321', '123'], + 7 => ['321', '123'], + ]; + } } diff --git a/tests/unit/framework/test/data/tbl_customer.php b/tests/unit/framework/test/data/tbl_customer.php index a1981d7450e..c5ccd4b3844 100644 --- a/tests/unit/framework/test/data/tbl_customer.php +++ b/tests/unit/framework/test/data/tbl_customer.php @@ -1,16 +1,16 @@ [ - 'email' => 'customer1@example.com', - 'name' => 'customer1', - 'address' => 'address1', - 'status' => 1, - ], - 'customer2' => [ - 'email' => 'customer2@example.com', - 'name' => 'customer2', - 'address' => 'address2', - 'status' => 2, - ], + 'customer1' => [ + 'email' => 'customer1@example.com', + 'name' => 'customer1', + 'address' => 'address1', + 'status' => 1, + ], + 'customer2' => [ + 'email' => 'customer2@example.com', + 'name' => 'customer2', + 'address' => 'address2', + 'status' => 2, + ], ]; diff --git a/tests/unit/framework/validators/BooleanValidatorTest.php b/tests/unit/framework/validators/BooleanValidatorTest.php index 5ed03074871..7447c4c0e28 100644 --- a/tests/unit/framework/validators/BooleanValidatorTest.php +++ b/tests/unit/framework/validators/BooleanValidatorTest.php @@ -10,50 +10,50 @@ */ class BooleanValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateValue() - { - $val = new BooleanValidator; - $this->assertTrue($val->validate(true)); - $this->assertTrue($val->validate(false)); - $this->assertTrue($val->validate('0')); - $this->assertTrue($val->validate('1')); - $this->assertFalse($val->validate(null)); - $this->assertFalse($val->validate([])); - $val->strict = true; - $this->assertTrue($val->validate('0')); - $this->assertTrue($val->validate('1')); - $this->assertFalse($val->validate(true)); - $this->assertFalse($val->validate(false)); - $val->trueValue = true; - $val->falseValue = false; - $this->assertFalse($val->validate('0')); - $this->assertFalse($val->validate([])); - $this->assertTrue($val->validate(true)); - $this->assertTrue($val->validate(false)); - } + public function testValidateValue() + { + $val = new BooleanValidator; + $this->assertTrue($val->validate(true)); + $this->assertTrue($val->validate(false)); + $this->assertTrue($val->validate('0')); + $this->assertTrue($val->validate('1')); + $this->assertFalse($val->validate(null)); + $this->assertFalse($val->validate([])); + $val->strict = true; + $this->assertTrue($val->validate('0')); + $this->assertTrue($val->validate('1')); + $this->assertFalse($val->validate(true)); + $this->assertFalse($val->validate(false)); + $val->trueValue = true; + $val->falseValue = false; + $this->assertFalse($val->validate('0')); + $this->assertFalse($val->validate([])); + $this->assertTrue($val->validate(true)); + $this->assertTrue($val->validate(false)); + } - public function testValidateAttributeAndError() - { - $obj = new FakedValidationModel; - $obj->attrA = true; - $obj->attrB = '1'; - $obj->attrC = '0'; - $obj->attrD = []; - $val = new BooleanValidator; - $val->validateAttribute($obj, 'attrA'); - $this->assertFalse($obj->hasErrors('attrA')); - $val->validateAttribute($obj, 'attrC'); - $this->assertFalse($obj->hasErrors('attrC')); - $val->strict = true; - $val->validateAttribute($obj, 'attrB'); - $this->assertFalse($obj->hasErrors('attrB')); - $val->validateAttribute($obj, 'attrD'); - $this->assertTrue($obj->hasErrors('attrD')); - } + public function testValidateAttributeAndError() + { + $obj = new FakedValidationModel; + $obj->attrA = true; + $obj->attrB = '1'; + $obj->attrC = '0'; + $obj->attrD = []; + $val = new BooleanValidator; + $val->validateAttribute($obj, 'attrA'); + $this->assertFalse($obj->hasErrors('attrA')); + $val->validateAttribute($obj, 'attrC'); + $this->assertFalse($obj->hasErrors('attrC')); + $val->strict = true; + $val->validateAttribute($obj, 'attrB'); + $this->assertFalse($obj->hasErrors('attrB')); + $val->validateAttribute($obj, 'attrD'); + $this->assertTrue($obj->hasErrors('attrD')); + } } diff --git a/tests/unit/framework/validators/CompareValidatorTest.php b/tests/unit/framework/validators/CompareValidatorTest.php index fb2a6386d64..c106539e231 100644 --- a/tests/unit/framework/validators/CompareValidatorTest.php +++ b/tests/unit/framework/validators/CompareValidatorTest.php @@ -8,166 +8,166 @@ class CompareValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateValueException() - { - $this->setExpectedException('yii\base\InvalidConfigException'); - $val = new CompareValidator; - $val->validate('val'); - } + public function testValidateValueException() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + $val = new CompareValidator; + $val->validate('val'); + } - public function testValidateValue() - { - $value = 18449; - // default config - $val = new CompareValidator(['compareValue' => $value]); - $this->assertTrue($val->validate($value)); - $this->assertTrue($val->validate((string)$value)); - $this->assertFalse($val->validate($value + 1)); - foreach ($this->getOperationTestData($value) as $op => $tests) { - $val = new CompareValidator(['compareValue' => $value]); - $val->operator = $op; - foreach ($tests as $test) { - $this->assertEquals($test[1], $val->validate($test[0])); - } - } - } + public function testValidateValue() + { + $value = 18449; + // default config + $val = new CompareValidator(['compareValue' => $value]); + $this->assertTrue($val->validate($value)); + $this->assertTrue($val->validate((string) $value)); + $this->assertFalse($val->validate($value + 1)); + foreach ($this->getOperationTestData($value) as $op => $tests) { + $val = new CompareValidator(['compareValue' => $value]); + $val->operator = $op; + foreach ($tests as $test) { + $this->assertEquals($test[1], $val->validate($test[0])); + } + } + } - protected function getOperationTestData($value) - { - return [ - '===' => [ - [$value, true], - [(string)$value, false], - [(float)$value, false], - [$value + 1, false], - ], - '!=' => [ - [$value, false], - [(string)$value, false], - [(float)$value, false], - [$value + 0.00001, true], - [false, true], - ], - '!==' => [ - [$value, false], - [(string)$value, true], - [(float)$value, true], - [false, true], - ], - '>' => [ - [$value, false], - [$value + 1, true], - [$value - 1, false], - ], - '>=' => [ - [$value, true], - [$value + 1, true], - [$value - 1, false], - ], - '<' => [ - [$value, false], - [$value + 1, false], - [$value - 1, true], - ], - '<=' => [ - [$value, true], - [$value + 1, false], - [$value - 1, true], - ], - //'non-op' => [ - // [$value, false], - // [$value + 1, false], - // [$value - 1, false], - //], - ]; - } + protected function getOperationTestData($value) + { + return [ + '===' => [ + [$value, true], + [(string) $value, false], + [(float) $value, false], + [$value + 1, false], + ], + '!=' => [ + [$value, false], + [(string) $value, false], + [(float) $value, false], + [$value + 0.00001, true], + [false, true], + ], + '!==' => [ + [$value, false], + [(string) $value, true], + [(float) $value, true], + [false, true], + ], + '>' => [ + [$value, false], + [$value + 1, true], + [$value - 1, false], + ], + '>=' => [ + [$value, true], + [$value + 1, true], + [$value - 1, false], + ], + '<' => [ + [$value, false], + [$value + 1, false], + [$value - 1, true], + ], + '<=' => [ + [$value, true], + [$value + 1, false], + [$value - 1, true], + ], + //'non-op' => [ + // [$value, false], + // [$value + 1, false], + // [$value - 1, false], + //], + ]; + } - public function testValidateAttribute() - { - // invalid-array - $val = new CompareValidator; - $model = new FakedValidationModel; - $model->attr = ['test_val']; - $val->validateAttribute($model, 'attr'); - $this->assertTrue($model->hasErrors('attr')); - $val = new CompareValidator(['compareValue' => 'test-string']); - $model = new FakedValidationModel; - $model->attr_test = 'test-string'; - $val->validateAttribute($model, 'attr_test'); - $this->assertFalse($model->hasErrors('attr_test')); - $val = new CompareValidator(['compareAttribute' => 'attr_test_val']); - $model = new FakedValidationModel; - $model->attr_test = 'test-string'; - $model->attr_test_val = 'test-string'; - $val->validateAttribute($model, 'attr_test'); - $this->assertFalse($model->hasErrors('attr_test')); - $this->assertFalse($model->hasErrors('attr_test_val')); - $val = new CompareValidator(['compareAttribute' => 'attr_test_val']); - $model = new FakedValidationModel; - $model->attr_test = 'test-string'; - $model->attr_test_val = 'test-string-false'; - $val->validateAttribute($model, 'attr_test'); - $this->assertTrue($model->hasErrors('attr_test')); - $this->assertFalse($model->hasErrors('attr_test_val')); - // assume: _repeat - $val = new CompareValidator; - $model = new FakedValidationModel; - $model->attr_test = 'test-string'; - $model->attr_test_repeat = 'test-string'; - $val->validateAttribute($model, 'attr_test'); - $this->assertFalse($model->hasErrors('attr_test')); - $this->assertFalse($model->hasErrors('attr_test_repeat')); - $val = new CompareValidator; - $model = new FakedValidationModel; - $model->attr_test = 'test-string'; - $model->attr_test_repeat = 'test-string2'; - $val->validateAttribute($model, 'attr_test'); - $this->assertTrue($model->hasErrors('attr_test')); - $this->assertFalse($model->hasErrors('attr_test_repeat')); - // not existing op - $val = new CompareValidator(); - $val->operator = '<>'; - $model = FakedValidationModel::createWithAttributes(['attr_o' => 5, 'attr_o_repeat' => 5]); - $val->validateAttribute($model, 'attr_o'); - $this->assertTrue($model->hasErrors('attr_o')); - } + public function testValidateAttribute() + { + // invalid-array + $val = new CompareValidator; + $model = new FakedValidationModel; + $model->attr = ['test_val']; + $val->validateAttribute($model, 'attr'); + $this->assertTrue($model->hasErrors('attr')); + $val = new CompareValidator(['compareValue' => 'test-string']); + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $val->validateAttribute($model, 'attr_test'); + $this->assertFalse($model->hasErrors('attr_test')); + $val = new CompareValidator(['compareAttribute' => 'attr_test_val']); + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_val = 'test-string'; + $val->validateAttribute($model, 'attr_test'); + $this->assertFalse($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_val')); + $val = new CompareValidator(['compareAttribute' => 'attr_test_val']); + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_val = 'test-string-false'; + $val->validateAttribute($model, 'attr_test'); + $this->assertTrue($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_val')); + // assume: _repeat + $val = new CompareValidator; + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_repeat = 'test-string'; + $val->validateAttribute($model, 'attr_test'); + $this->assertFalse($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_repeat')); + $val = new CompareValidator; + $model = new FakedValidationModel; + $model->attr_test = 'test-string'; + $model->attr_test_repeat = 'test-string2'; + $val->validateAttribute($model, 'attr_test'); + $this->assertTrue($model->hasErrors('attr_test')); + $this->assertFalse($model->hasErrors('attr_test_repeat')); + // not existing op + $val = new CompareValidator(); + $val->operator = '<>'; + $model = FakedValidationModel::createWithAttributes(['attr_o' => 5, 'attr_o_repeat' => 5]); + $val->validateAttribute($model, 'attr_o'); + $this->assertTrue($model->hasErrors('attr_o')); + } - public function testValidateAttributeOperators() - { - $value = 55; - foreach ($this->getOperationTestData($value) as $operator => $tests) { - $val = new CompareValidator(['operator' => $operator, 'compareValue' => $value]); - foreach ($tests as $test) { - $model = new FakedValidationModel; - $model->attr_test = $test[0]; - $val->validateAttribute($model, 'attr_test'); - $this->assertEquals($test[1], !$model->hasErrors('attr_test')); - } + public function testValidateAttributeOperators() + { + $value = 55; + foreach ($this->getOperationTestData($value) as $operator => $tests) { + $val = new CompareValidator(['operator' => $operator, 'compareValue' => $value]); + foreach ($tests as $test) { + $model = new FakedValidationModel; + $model->attr_test = $test[0]; + $val->validateAttribute($model, 'attr_test'); + $this->assertEquals($test[1], !$model->hasErrors('attr_test')); + } - } - } + } + } - public function testEnsureMessageSetOnInit() - { - foreach ($this->getOperationTestData(1337) as $operator => $tests) { - $val = new CompareValidator(['operator' => $operator]); - $this->assertTrue(strlen($val->message) > 1); - } - try { - new CompareValidator(['operator' => '<>']); - } catch (InvalidConfigException $e) { - return; - } - catch (\Exception $e) { - $this->fail('InvalidConfigException expected' . get_class($e) . 'received'); - return; - } - $this->fail('InvalidConfigException expected none received'); - } + public function testEnsureMessageSetOnInit() + { + foreach ($this->getOperationTestData(1337) as $operator => $tests) { + $val = new CompareValidator(['operator' => $operator]); + $this->assertTrue(strlen($val->message) > 1); + } + try { + new CompareValidator(['operator' => '<>']); + } catch (InvalidConfigException $e) { + return; + } catch (\Exception $e) { + $this->fail('InvalidConfigException expected' . get_class($e) . 'received'); + + return; + } + $this->fail('InvalidConfigException expected none received'); + } } diff --git a/tests/unit/framework/validators/DateValidatorTest.php b/tests/unit/framework/validators/DateValidatorTest.php index 54ec03ce5e7..c24ec2ef1a7 100644 --- a/tests/unit/framework/validators/DateValidatorTest.php +++ b/tests/unit/framework/validators/DateValidatorTest.php @@ -9,62 +9,62 @@ class DateValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testEnsureMessageIsSet() - { - $val = new DateValidator; - $this->assertTrue($val->message !== null && strlen($val->message) > 1); - } + public function testEnsureMessageIsSet() + { + $val = new DateValidator; + $this->assertTrue($val->message !== null && strlen($val->message) > 1); + } - public function testValidateValue() - { - $val = new DateValidator; - $this->assertFalse($val->validate('3232-32-32')); - $this->assertTrue($val->validate('2013-09-13')); - $this->assertFalse($val->validate('31.7.2013')); - $this->assertFalse($val->validate('31-7-2013')); - $this->assertFalse($val->validate(time())); - $val->format = 'U'; - $this->assertTrue($val->validate(time())); - $val->format = 'd.m.Y'; - $this->assertTrue($val->validate('31.7.2013')); - $val->format = 'Y-m-!d H:i:s'; - $this->assertTrue($val->validate('2009-02-15 15:16:17')); - } + public function testValidateValue() + { + $val = new DateValidator; + $this->assertFalse($val->validate('3232-32-32')); + $this->assertTrue($val->validate('2013-09-13')); + $this->assertFalse($val->validate('31.7.2013')); + $this->assertFalse($val->validate('31-7-2013')); + $this->assertFalse($val->validate(time())); + $val->format = 'U'; + $this->assertTrue($val->validate(time())); + $val->format = 'd.m.Y'; + $this->assertTrue($val->validate('31.7.2013')); + $val->format = 'Y-m-!d H:i:s'; + $this->assertTrue($val->validate('2009-02-15 15:16:17')); + } - public function testValidateAttribute() - { - // error-array-add - $val = new DateValidator; - $model = new FakedValidationModel; - $model->attr_date = '2013-09-13'; - $val->validateAttribute($model, 'attr_date'); - $this->assertFalse($model->hasErrors('attr_date')); - $model = new FakedValidationModel; - $model->attr_date = '1375293913'; - $val->validateAttribute($model, 'attr_date'); - $this->assertTrue($model->hasErrors('attr_date')); - //// timestamp attribute - $val = new DateValidator(['timestampAttribute' => 'attr_timestamp']); - $model = new FakedValidationModel; - $model->attr_date = '2013-09-13'; - $model->attr_timestamp = true; - $val->validateAttribute($model, 'attr_date'); - $this->assertFalse($model->hasErrors('attr_date')); - $this->assertFalse($model->hasErrors('attr_timestamp')); - $this->assertEquals( - DateTime::createFromFormat($val->format, '2013-09-13')->getTimestamp(), - $model->attr_timestamp - ); - $val = new DateValidator(); - $model = FakedValidationModel::createWithAttributes(['attr_date' => []]); - $val->validateAttribute($model, 'attr_date'); - $this->assertTrue($model->hasErrors('attr_date')); + public function testValidateAttribute() + { + // error-array-add + $val = new DateValidator; + $model = new FakedValidationModel; + $model->attr_date = '2013-09-13'; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $model = new FakedValidationModel; + $model->attr_date = '1375293913'; + $val->validateAttribute($model, 'attr_date'); + $this->assertTrue($model->hasErrors('attr_date')); + //// timestamp attribute + $val = new DateValidator(['timestampAttribute' => 'attr_timestamp']); + $model = new FakedValidationModel; + $model->attr_date = '2013-09-13'; + $model->attr_timestamp = true; + $val->validateAttribute($model, 'attr_date'); + $this->assertFalse($model->hasErrors('attr_date')); + $this->assertFalse($model->hasErrors('attr_timestamp')); + $this->assertEquals( + DateTime::createFromFormat($val->format, '2013-09-13')->getTimestamp(), + $model->attr_timestamp + ); + $val = new DateValidator(); + $model = FakedValidationModel::createWithAttributes(['attr_date' => []]); + $val->validateAttribute($model, 'attr_date'); + $this->assertTrue($model->hasErrors('attr_date')); - } + } } diff --git a/tests/unit/framework/validators/DefaultValueValidatorTest.php b/tests/unit/framework/validators/DefaultValueValidatorTest.php index fb2bf52a7d9..cfa0f00a6ac 100644 --- a/tests/unit/framework/validators/DefaultValueValidatorTest.php +++ b/tests/unit/framework/validators/DefaultValueValidatorTest.php @@ -10,31 +10,31 @@ */ class DefaultValueValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateAttribute() - { - $val = new DefaultValueValidator; - $val->value = 'test_value'; - $obj = new \stdclass; - $obj->attrA = 'attrA'; - $obj->attrB = null; - $obj->attrC = ''; - // original values to chek which attritubes where modified - $objB = clone $obj; - $val->validateAttribute($obj, 'attrB'); - $this->assertEquals($val->value, $obj->attrB); - $this->assertEquals($objB->attrA, $obj->attrA); - $val->value = 'new_test_value'; - $obj = clone $objB; // get clean object - $val->validateAttribute($obj, 'attrC'); - $this->assertEquals('new_test_value', $obj->attrC); - $this->assertEquals($objB->attrA, $obj->attrA); - $val->validateAttribute($obj, 'attrA'); - $this->assertEquals($objB->attrA, $obj->attrA); - } + public function testValidateAttribute() + { + $val = new DefaultValueValidator; + $val->value = 'test_value'; + $obj = new \stdclass; + $obj->attrA = 'attrA'; + $obj->attrB = null; + $obj->attrC = ''; + // original values to chek which attritubes where modified + $objB = clone $obj; + $val->validateAttribute($obj, 'attrB'); + $this->assertEquals($val->value, $obj->attrB); + $this->assertEquals($objB->attrA, $obj->attrA); + $val->value = 'new_test_value'; + $obj = clone $objB; // get clean object + $val->validateAttribute($obj, 'attrC'); + $this->assertEquals('new_test_value', $obj->attrC); + $this->assertEquals($objB->attrA, $obj->attrA); + $val->validateAttribute($obj, 'attrA'); + $this->assertEquals($objB->attrA, $obj->attrA); + } } diff --git a/tests/unit/framework/validators/EmailValidatorTest.php b/tests/unit/framework/validators/EmailValidatorTest.php index 3fcd2dd1941..929914db8d0 100644 --- a/tests/unit/framework/validators/EmailValidatorTest.php +++ b/tests/unit/framework/validators/EmailValidatorTest.php @@ -11,98 +11,99 @@ */ class EmailValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateValue() - { - $validator = new EmailValidator(); + public function testValidateValue() + { + $validator = new EmailValidator(); - $this->assertTrue($validator->validate('sam@rmcreative.ru')); - $this->assertTrue($validator->validate('5011@gmail.com')); - $this->assertFalse($validator->validate('rmcreative.ru')); - $this->assertFalse($validator->validate('Carsten Brandt ')); - $this->assertFalse($validator->validate('"Carsten Brandt" ')); - $this->assertFalse($validator->validate('')); - $this->assertFalse($validator->validate('info@örtliches.de')); - $this->assertFalse($validator->validate('sam@рмкреатиф.ru')); + $this->assertTrue($validator->validate('sam@rmcreative.ru')); + $this->assertTrue($validator->validate('5011@gmail.com')); + $this->assertFalse($validator->validate('rmcreative.ru')); + $this->assertFalse($validator->validate('Carsten Brandt ')); + $this->assertFalse($validator->validate('"Carsten Brandt" ')); + $this->assertFalse($validator->validate('')); + $this->assertFalse($validator->validate('info@örtliches.de')); + $this->assertFalse($validator->validate('sam@рмкреатиф.ru')); - $validator->allowName = true; + $validator->allowName = true; - $this->assertTrue($validator->validate('sam@rmcreative.ru')); - $this->assertTrue($validator->validate('5011@gmail.com')); - $this->assertFalse($validator->validate('rmcreative.ru')); - $this->assertTrue($validator->validate('Carsten Brandt ')); - $this->assertTrue($validator->validate('"Carsten Brandt" ')); - $this->assertTrue($validator->validate('')); - $this->assertFalse($validator->validate('info@örtliches.de')); - $this->assertFalse($validator->validate('sam@рмкреатиф.ru')); - $this->assertFalse($validator->validate('Informtation info@oertliches.de')); - $this->assertTrue($validator->validate('test@example.com')); - $this->assertTrue($validator->validate('John Smith ')); - $this->assertFalse($validator->validate('John Smith ')); - } + $this->assertTrue($validator->validate('sam@rmcreative.ru')); + $this->assertTrue($validator->validate('5011@gmail.com')); + $this->assertFalse($validator->validate('rmcreative.ru')); + $this->assertTrue($validator->validate('Carsten Brandt ')); + $this->assertTrue($validator->validate('"Carsten Brandt" ')); + $this->assertTrue($validator->validate('')); + $this->assertFalse($validator->validate('info@örtliches.de')); + $this->assertFalse($validator->validate('sam@рмкреатиф.ru')); + $this->assertFalse($validator->validate('Informtation info@oertliches.de')); + $this->assertTrue($validator->validate('test@example.com')); + $this->assertTrue($validator->validate('John Smith ')); + $this->assertFalse($validator->validate('John Smith ')); + } - public function testValidateValueIdn() - { - if (!function_exists('idn_to_ascii')) { - $this->markTestSkipped('Intl extension required'); - return; - } - $validator = new EmailValidator(); - $validator->enableIDN = true; + public function testValidateValueIdn() + { + if (!function_exists('idn_to_ascii')) { + $this->markTestSkipped('Intl extension required'); - $this->assertTrue($validator->validate('5011@example.com')); - $this->assertTrue($validator->validate('example@äüößìà.de')); - $this->assertTrue($validator->validate('example@xn--zcack7ayc9a.de')); - $this->assertTrue($validator->validate('info@örtliches.de')); - $this->assertTrue($validator->validate('sam@рмкреатиф.ru')); - $this->assertTrue($validator->validate('sam@rmcreative.ru')); - $this->assertTrue($validator->validate('5011@gmail.com')); - $this->assertFalse($validator->validate('rmcreative.ru')); - $this->assertFalse($validator->validate('Carsten Brandt ')); - $this->assertFalse($validator->validate('"Carsten Brandt" ')); - $this->assertFalse($validator->validate('')); + return; + } + $validator = new EmailValidator(); + $validator->enableIDN = true; - $validator->allowName = true; + $this->assertTrue($validator->validate('5011@example.com')); + $this->assertTrue($validator->validate('example@äüößìà.de')); + $this->assertTrue($validator->validate('example@xn--zcack7ayc9a.de')); + $this->assertTrue($validator->validate('info@örtliches.de')); + $this->assertTrue($validator->validate('sam@рмкреатиф.ru')); + $this->assertTrue($validator->validate('sam@rmcreative.ru')); + $this->assertTrue($validator->validate('5011@gmail.com')); + $this->assertFalse($validator->validate('rmcreative.ru')); + $this->assertFalse($validator->validate('Carsten Brandt ')); + $this->assertFalse($validator->validate('"Carsten Brandt" ')); + $this->assertFalse($validator->validate('')); - $this->assertTrue($validator->validate('info@örtliches.de')); - $this->assertTrue($validator->validate('Informtation ')); - $this->assertFalse($validator->validate('Informtation info@örtliches.de')); - $this->assertTrue($validator->validate('sam@рмкреатиф.ru')); - $this->assertTrue($validator->validate('sam@rmcreative.ru')); - $this->assertTrue($validator->validate('5011@gmail.com')); - $this->assertFalse($validator->validate('rmcreative.ru')); - $this->assertTrue($validator->validate('Carsten Brandt ')); - $this->assertTrue($validator->validate('"Carsten Brandt" ')); - $this->assertTrue($validator->validate('')); - $this->assertTrue($validator->validate('test@example.com')); - $this->assertTrue($validator->validate('John Smith ')); - $this->assertFalse($validator->validate('John Smith ')); - } + $validator->allowName = true; - public function testValidateValueMx() - { - $validator = new EmailValidator(); + $this->assertTrue($validator->validate('info@örtliches.de')); + $this->assertTrue($validator->validate('Informtation ')); + $this->assertFalse($validator->validate('Informtation info@örtliches.de')); + $this->assertTrue($validator->validate('sam@рмкреатиф.ru')); + $this->assertTrue($validator->validate('sam@rmcreative.ru')); + $this->assertTrue($validator->validate('5011@gmail.com')); + $this->assertFalse($validator->validate('rmcreative.ru')); + $this->assertTrue($validator->validate('Carsten Brandt ')); + $this->assertTrue($validator->validate('"Carsten Brandt" ')); + $this->assertTrue($validator->validate('')); + $this->assertTrue($validator->validate('test@example.com')); + $this->assertTrue($validator->validate('John Smith ')); + $this->assertFalse($validator->validate('John Smith ')); + } - $validator->checkDNS = true; - $this->assertTrue($validator->validate('5011@gmail.com')); + public function testValidateValueMx() + { + $validator = new EmailValidator(); - $validator->checkDNS = false; - $this->assertTrue($validator->validate('test@nonexistingsubdomain.example.com')); - $validator->checkDNS = true; - $this->assertFalse($validator->validate('test@nonexistingsubdomain.example.com')); - } + $validator->checkDNS = true; + $this->assertTrue($validator->validate('5011@gmail.com')); - public function testValidateAttribute() - { - $val = new EmailValidator(); - $model = new FakedValidationModel(); - $model->attr_email = '5011@gmail.com'; - $val->validateAttribute($model, 'attr_email'); - $this->assertFalse($model->hasErrors('attr_email')); - } + $validator->checkDNS = false; + $this->assertTrue($validator->validate('test@nonexistingsubdomain.example.com')); + $validator->checkDNS = true; + $this->assertFalse($validator->validate('test@nonexistingsubdomain.example.com')); + } + + public function testValidateAttribute() + { + $val = new EmailValidator(); + $model = new FakedValidationModel(); + $model->attr_email = '5011@gmail.com'; + $val->validateAttribute($model, 'attr_email'); + $this->assertFalse($model->hasErrors('attr_email')); + } } diff --git a/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php b/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php index 6ed66affa36..2e8d8940714 100644 --- a/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php +++ b/tests/unit/framework/validators/ExistValidatorDriverTests/ExistValidatorPostgresTest.php @@ -1,10 +1,9 @@ mockApplication(); - ActiveRecord::$db = $this->getConnection(); - } + public function setUp() + { + parent::setUp(); + $this->mockApplication(); + ActiveRecord::$db = $this->getConnection(); + } - public function testValidateValueExpectedException() - { - try { - $val = new ExistValidator(); - $val->validate('ref'); - $this->fail('Exception should have been thrown at this time'); - } catch (Exception $e) { - $this->assertInstanceOf('yii\base\InvalidConfigException', $e); - $this->assertEquals('The "targetClass" property must be set.', $e->getMessage()); - } - // combine to save the time creating a new db-fixture set (likely ~5 sec) - try { - $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className()]); - $val->validate('ref'); - $this->fail('Exception should have been thrown at this time'); - } catch (Exception $e) { - $this->assertInstanceOf('yii\base\InvalidConfigException', $e); - $this->assertEquals('The "targetAttribute" property must be configured as a string.', $e->getMessage()); - } - } + public function testValidateValueExpectedException() + { + try { + $val = new ExistValidator(); + $val->validate('ref'); + $this->fail('Exception should have been thrown at this time'); + } catch (Exception $e) { + $this->assertInstanceOf('yii\base\InvalidConfigException', $e); + $this->assertEquals('The "targetClass" property must be set.', $e->getMessage()); + } + // combine to save the time creating a new db-fixture set (likely ~5 sec) + try { + $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className()]); + $val->validate('ref'); + $this->fail('Exception should have been thrown at this time'); + } catch (Exception $e) { + $this->assertInstanceOf('yii\base\InvalidConfigException', $e); + $this->assertEquals('The "targetAttribute" property must be configured as a string.', $e->getMessage()); + } + } - public function testValidateValue() - { - $val = new ExistValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'id']); - $this->assertTrue($val->validate(2)); - $this->assertTrue($val->validate(5)); - $this->assertFalse($val->validate(99)); - $this->assertFalse($val->validate(['1'])); - } + public function testValidateValue() + { + $val = new ExistValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'id']); + $this->assertTrue($val->validate(2)); + $this->assertTrue($val->validate(5)); + $this->assertFalse($val->validate(99)); + $this->assertFalse($val->validate(['1'])); + } - public function testValidateAttribute() - { - // existing value on different table - $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']); - $m = ValidatorTestRefModel::find(['id' => 1]); - $val->validateAttribute($m, 'ref'); - $this->assertFalse($m->hasErrors()); - // non-existing value on different table - $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']); - $m = ValidatorTestRefModel::find(['id' => 6]); - $val->validateAttribute($m, 'ref'); - $this->assertTrue($m->hasErrors('ref')); - // existing value on same table - $val = new ExistValidator(['targetAttribute' => 'ref']); - $m = ValidatorTestRefModel::find(['id' => 2]); - $val->validateAttribute($m, 'test_val'); - $this->assertFalse($m->hasErrors()); - // non-existing value on same table - $val = new ExistValidator(['targetAttribute' => 'ref']); - $m = ValidatorTestRefModel::find(['id' => 5]); - $val->validateAttribute($m, 'test_val_fail'); - $this->assertTrue($m->hasErrors('test_val_fail')); - // check for given value (true) - $val = new ExistValidator(); - $m = ValidatorTestRefModel::find(['id' => 3]); - $val->validateAttribute($m, 'ref'); - $this->assertFalse($m->hasErrors()); - // check for given defaults (false) - $val = new ExistValidator(); - $m = ValidatorTestRefModel::find(['id' => 4]); - $m->a_field = 'some new value'; - $val->validateAttribute($m, 'a_field'); - $this->assertTrue($m->hasErrors('a_field')); - // check array - $val = new ExistValidator(['targetAttribute' => 'ref']); - $m = ValidatorTestRefModel::find(['id' => 2]); - $m->test_val = [1,2,3]; - $val->validateAttribute($m, 'test_val'); - $this->assertTrue($m->hasErrors('test_val')); - } + public function testValidateAttribute() + { + // existing value on different table + $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']); + $m = ValidatorTestRefModel::find(['id' => 1]); + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors()); + // non-existing value on different table + $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']); + $m = ValidatorTestRefModel::find(['id' => 6]); + $val->validateAttribute($m, 'ref'); + $this->assertTrue($m->hasErrors('ref')); + // existing value on same table + $val = new ExistValidator(['targetAttribute' => 'ref']); + $m = ValidatorTestRefModel::find(['id' => 2]); + $val->validateAttribute($m, 'test_val'); + $this->assertFalse($m->hasErrors()); + // non-existing value on same table + $val = new ExistValidator(['targetAttribute' => 'ref']); + $m = ValidatorTestRefModel::find(['id' => 5]); + $val->validateAttribute($m, 'test_val_fail'); + $this->assertTrue($m->hasErrors('test_val_fail')); + // check for given value (true) + $val = new ExistValidator(); + $m = ValidatorTestRefModel::find(['id' => 3]); + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors()); + // check for given defaults (false) + $val = new ExistValidator(); + $m = ValidatorTestRefModel::find(['id' => 4]); + $m->a_field = 'some new value'; + $val->validateAttribute($m, 'a_field'); + $this->assertTrue($m->hasErrors('a_field')); + // check array + $val = new ExistValidator(['targetAttribute' => 'ref']); + $m = ValidatorTestRefModel::find(['id' => 2]); + $m->test_val = [1,2,3]; + $val->validateAttribute($m, 'test_val'); + $this->assertTrue($m->hasErrors('test_val')); + } - public function testValidateCompositeKeys() - { - $val = new ExistValidator([ - 'targetClass' => OrderItem::className(), - 'targetAttribute' => ['order_id', 'item_id'], - ]); - // validate old record - $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); - $val->validateAttribute($m, 'order_id'); - $this->assertFalse($m->hasErrors('order_id')); + public function testValidateCompositeKeys() + { + $val = new ExistValidator([ + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['order_id', 'item_id'], + ]); + // validate old record + $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); - // validate new record - $m = new OrderItem(['order_id' => 1, 'item_id' => 2]); - $val->validateAttribute($m, 'order_id'); - $this->assertFalse($m->hasErrors('order_id')); - $m = new OrderItem(['order_id' => 10, 'item_id' => 2]); - $val->validateAttribute($m, 'order_id'); - $this->assertTrue($m->hasErrors('order_id')); + // validate new record + $m = new OrderItem(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); + $m = new OrderItem(['order_id' => 10, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertTrue($m->hasErrors('order_id')); - $val = new ExistValidator([ - 'targetClass' => OrderItem::className(), - 'targetAttribute' => ['id' => 'order_id'], - ]); - // validate old record - $m = Order::find(1); - $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); - $m = Order::find(1); - $m->id = 10; - $val->validateAttribute($m, 'id'); - $this->assertTrue($m->hasErrors('id')); + $val = new ExistValidator([ + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['id' => 'order_id'], + ]); + // validate old record + $m = Order::find(1); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m = Order::find(1); + $m->id = 10; + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); - $m = new Order(['id' => 1]); - $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); - $m = new Order(['id' => 10]); - $val->validateAttribute($m, 'id'); - $this->assertTrue($m->hasErrors('id')); - } + $m = new Order(['id' => 1]); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m = new Order(['id' => 10]); + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + } } diff --git a/tests/unit/framework/validators/FileValidatorTest.php b/tests/unit/framework/validators/FileValidatorTest.php index b6ba11cacd9..bc790d11ec4 100644 --- a/tests/unit/framework/validators/FileValidatorTest.php +++ b/tests/unit/framework/validators/FileValidatorTest.php @@ -2,7 +2,6 @@ namespace yiiunit\framework\validators; - use yii\validators\FileValidator; use yii\web\UploadedFile; use Yii; @@ -11,332 +10,334 @@ class FileValidatorTest extends TestCase { - public function setUp() - { - $this->mockApplication(); - } + public function setUp() + { + $this->mockApplication(); + } + + public function testAssureMessagesSetOnInit() + { + $val = new FileValidator(); + foreach (['message', 'uploadRequired', 'tooMany', 'wrongType', 'tooBig', 'tooSmall'] as $attr) { + $this->assertTrue(is_string($val->$attr)); + } + } + + public function testTypeSplitOnInit() + { + $val = new FileValidator(['types' => 'jpeg, jpg, gif']); + $this->assertEquals(['jpeg', 'jpg', 'gif'], $val->types); + $val = new FileValidator(['types' => 'jpeg']); + $this->assertEquals(['jpeg'], $val->types); + $val = new FileValidator(['types' => '']); + $this->assertEquals([], $val->types); + $val = new FileValidator(['types' => []]); + $this->assertEquals([], $val->types); + $val = new FileValidator(); + $this->assertEquals([], $val->types); + $val = new FileValidator(['types' => ['jpeg', 'exe']]); + $this->assertEquals(['jpeg', 'exe'], $val->types); + } - public function testAssureMessagesSetOnInit() - { - $val = new FileValidator(); - foreach (['message', 'uploadRequired', 'tooMany', 'wrongType', 'tooBig', 'tooSmall'] as $attr) { - $this->assertTrue(is_string($val->$attr)); - } - } + public function testGetSizeLimit() + { + $size = $this->sizeToBytes(ini_get('upload_max_filesize')); + $val = new FileValidator(); + $this->assertEquals($size, $val->getSizeLimit()); + $val->maxSize = $size + 1; // set and test if value is overridden + $this->assertEquals($size, $val->getSizeLimit()); + $val->maxSize = abs($size - 1); + $this->assertEquals($size - 1, $val->getSizeLimit()); + $_POST['MAX_FILE_SIZE'] = $size + 1; + $this->assertEquals($size - 1, $val->getSizeLimit()); + $_POST['MAX_FILE_SIZE'] = abs($size - 2); + $this->assertSame($_POST['MAX_FILE_SIZE'], $val->getSizeLimit()); + } - public function testTypeSplitOnInit() - { - $val = new FileValidator(['types' => 'jpeg, jpg, gif']); - $this->assertEquals(['jpeg', 'jpg', 'gif'], $val->types); - $val = new FileValidator(['types' => 'jpeg']); - $this->assertEquals(['jpeg'], $val->types); - $val = new FileValidator(['types' => '']); - $this->assertEquals([], $val->types); - $val = new FileValidator(['types' => []]); - $this->assertEquals([], $val->types); - $val = new FileValidator(); - $this->assertEquals([], $val->types); - $val = new FileValidator(['types' => ['jpeg', 'exe']]); - $this->assertEquals(['jpeg', 'exe'], $val->types); - } + protected function sizeToBytes($sizeStr) + { + switch (substr($sizeStr, -1)) { + case 'M': + case 'm': + return (int) $sizeStr * 1048576; + case 'K': + case 'k': + return (int) $sizeStr * 1024; + case 'G': + case 'g': + return (int) $sizeStr * 1073741824; + default: + return (int) $sizeStr; + } + } - public function testGetSizeLimit() - { - $size = $this->sizeToBytes(ini_get('upload_max_filesize')); - $val = new FileValidator(); - $this->assertEquals($size, $val->getSizeLimit()); - $val->maxSize = $size + 1; // set and test if value is overridden - $this->assertEquals($size, $val->getSizeLimit()); - $val->maxSize = abs($size - 1); - $this->assertEquals($size - 1, $val->getSizeLimit()); - $_POST['MAX_FILE_SIZE'] = $size + 1; - $this->assertEquals($size - 1, $val->getSizeLimit()); - $_POST['MAX_FILE_SIZE'] = abs($size - 2); - $this->assertSame($_POST['MAX_FILE_SIZE'], $val->getSizeLimit()); - } + public function testValidateAttributeMultiple() + { + $val = new FileValidator(['maxFiles' => 2]); + $m = FakedValidationModel::createWithAttributes(['attr_files' => 'path']); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $m = FakedValidationModel::createWithAttributes(['attr_files' => []]); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files'))); + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_files' => $this->createTestFiles( + [ + [ + 'name' => 'test_up_1.txt', + 'size' => 1024, + ], + [ + 'error' => UPLOAD_ERR_NO_FILE, + ], + ] + ) + ] + ); + $val->validateAttribute($m, 'attr_files'); + $this->assertFalse($m->hasErrors('attr_files')); + $m = FakedValidationModel::createWithAttributes([ + 'attr_files' => $this->createTestFiles([ + [''], [''], [''] + ]) + ]); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors()); + $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'you can upload at most') !== false); + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_images' => $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png' + ], + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png' + ], + [ + 'name' => 'text.txt', + 'size' => 1024 + ], + ] + ) + ] + ); + $m->setScenario('validateMultipleFiles'); + $this->assertFalse($m->validate()); + $this->assertTrue(stripos(current($m->getErrors('attr_images')), + 'Only files with these extensions are allowed') !== false); - protected function sizeToBytes($sizeStr) - { - switch (substr($sizeStr, -1)) { - case 'M': - case 'm': - return (int)$sizeStr * 1048576; - case 'K': - case 'k': - return (int)$sizeStr * 1024; - case 'G': - case 'g': - return (int)$sizeStr * 1073741824; - default: - return (int)$sizeStr; - } - } + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_images' => $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png' + ], + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png' + ], + ] + ) + ] + ); + $m->setScenario('validateMultipleFiles'); + $this->assertTrue($m->validate()); - public function testValidateAttributeMultiple() - { - $val = new FileValidator(['maxFiles' => 2]); - $m = FakedValidationModel::createWithAttributes(['attr_files' => 'path']); - $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors('attr_files')); - $m = FakedValidationModel::createWithAttributes(['attr_files' => []]); - $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors('attr_files')); - $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files'))); - $m = FakedValidationModel::createWithAttributes( - [ - 'attr_files' => $this->createTestFiles( - [ - [ - 'name' => 'test_up_1.txt', - 'size' => 1024, - ], - [ - 'error' => UPLOAD_ERR_NO_FILE, - ], - ] - ) - ] - ); - $val->validateAttribute($m, 'attr_files'); - $this->assertFalse($m->hasErrors('attr_files')); - $m = FakedValidationModel::createWithAttributes([ - 'attr_files' => $this->createTestFiles([ - [''], [''], [''] - ]) - ]); - $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors()); - $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'you can upload at most') !== false); - $m = FakedValidationModel::createWithAttributes( - [ - 'attr_images' => $this->createTestFiles( - [ - [ - 'name' => 'image.png', - 'size' => 1024, - 'type' => 'image/png' - ], - [ - 'name' => 'image.png', - 'size' => 1024, - 'type' => 'image/png' - ], - [ - 'name' => 'text.txt', - 'size' => 1024 - ], - ] - ) - ] - ); - $m->setScenario('validateMultipleFiles'); - $this->assertFalse($m->validate()); - $this->assertTrue(stripos(current($m->getErrors('attr_images')), - 'Only files with these extensions are allowed') !== false); + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_image' => $this->createTestFiles( + [ + [ + 'name' => 'text.txt', + 'size' => 1024, + ], + ] + ) + ] + ); + $m->setScenario('validateFile'); + $this->assertFalse($m->validate()); + } - $m = FakedValidationModel::createWithAttributes( - [ - 'attr_images' => $this->createTestFiles( - [ - [ - 'name' => 'image.png', - 'size' => 1024, - 'type' => 'image/png' - ], - [ - 'name' => 'image.png', - 'size' => 1024, - 'type' => 'image/png' - ], - ] - ) - ] - ); - $m->setScenario('validateMultipleFiles'); - $this->assertTrue($m->validate()); + /** + * @param array $params + * @return UploadedFile[] + */ + protected function createTestFiles($params = []) + { + $rndString = function ($len = 10) { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $randomString = ''; + for ($i = 0; $i < $len; $i++) { + $randomString .= $characters[rand(0, strlen($characters) - 1)]; + } - $m = FakedValidationModel::createWithAttributes( - [ - 'attr_image' => $this->createTestFiles( - [ - [ - 'name' => 'text.txt', - 'size' => 1024, - ], - ] - ) - ] - ); - $m->setScenario('validateFile'); - $this->assertFalse($m->validate()); - } + return $randomString; + }; + $files = []; + foreach ($params as $param) { + if (empty($param) && count($params) != 1) { + $files[] = ['no instance of UploadedFile']; + continue; + } + $name = isset($param['name']) ? $param['name'] : $rndString(); + $tempName = \Yii::getAlias('@yiiunit/runtime/validators/file/tmp') . $name; + if (is_readable($tempName)) { + $size = filesize($tempName); + } else { + $size = isset($param['size']) ? $param['size'] : rand( + 1, + $this->sizeToBytes(ini_get('upload_max_filesize')) + ); + } + $type = isset($param['type']) ? $param['type'] : 'text/plain'; + $error = isset($param['error']) ? $param['error'] : UPLOAD_ERR_OK; + if (count($params) == 1) { + $error = empty($param) ? UPLOAD_ERR_NO_FILE : $error; - /** - * @param array $params - * @return UploadedFile[] - */ - protected function createTestFiles($params = []) - { - $rndString = function ($len = 10) { - $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $randomString = ''; - for ($i = 0; $i < $len; $i++) { - $randomString .= $characters[rand(0, strlen($characters) - 1)]; - } - return $randomString; - }; - $files = []; - foreach ($params as $param) { - if (empty($param) && count($params) != 1) { - $files[] = ['no instance of UploadedFile']; - continue; - } - $name = isset($param['name']) ? $param['name'] : $rndString(); - $tempName = \Yii::getAlias('@yiiunit/runtime/validators/file/tmp') . $name; - if (is_readable($tempName)) { - $size = filesize($tempName); - } else { - $size = isset($param['size']) ? $param['size'] : rand( - 1, - $this->sizeToBytes(ini_get('upload_max_filesize')) - ); - } - $type = isset($param['type']) ? $param['type'] : 'text/plain'; - $error = isset($param['error']) ? $param['error'] : UPLOAD_ERR_OK; - if (count($params) == 1) { - $error = empty($param) ? UPLOAD_ERR_NO_FILE : $error; - return new UploadedFile([ - 'name' => $name, - 'tempName' => $tempName, - 'type' => $type, - 'size' => $size, - 'error' => $error - ]); - } - $files[] = new UploadedFile([ - 'name' => $name, - 'tempName' => $tempName, - 'type' => $type, - 'size' => $size, - 'error' => $error - ]); - } - return $files; - } + return new UploadedFile([ + 'name' => $name, + 'tempName' => $tempName, + 'type' => $type, + 'size' => $size, + 'error' => $error + ]); + } + $files[] = new UploadedFile([ + 'name' => $name, + 'tempName' => $tempName, + 'type' => $type, + 'size' => $size, + 'error' => $error + ]); + } - public function testValidateAttribute() - { - // single File - $val = new FileValidator(); - $m = $this->createModelForAttributeTest(); - $val->validateAttribute($m, 'attr_files'); - $this->assertFalse($m->hasErrors()); - $val->validateAttribute($m, 'attr_files_empty'); - $this->assertTrue($m->hasErrors('attr_files_empty')); - $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); + return $files; + } - // single File with skipOnEmpty = false - $val = new FileValidator(['skipOnEmpty' => false]); - $m = $this->createModelForAttributeTest(); - $val->validateAttribute($m, 'attr_files'); - $this->assertFalse($m->hasErrors()); - $val->validateAttribute($m, 'attr_files_empty'); - $this->assertTrue($m->hasErrors('attr_files_empty')); - $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); - $m = $this->createModelForAttributeTest(); + public function testValidateAttribute() + { + // single File + $val = new FileValidator(); + $m = $this->createModelForAttributeTest(); + $val->validateAttribute($m, 'attr_files'); + $this->assertFalse($m->hasErrors()); + $val->validateAttribute($m, 'attr_files_empty'); + $this->assertTrue($m->hasErrors('attr_files_empty')); + $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); - // too big - $val = new FileValidator(['maxSize' => 128]); - $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors('attr_files')); - $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'too big') !== false); - // to Small - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(['minSize' => 2048]); - $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors('attr_files')); - $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'too small') !== false); - // UPLOAD_ERR_INI_SIZE/UPLOAD_ERR_FORM_SIZE - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(); - $val->validateAttribute($m, 'attr_err_ini'); - $this->assertTrue($m->hasErrors('attr_err_ini')); - $this->assertTrue(stripos(current($m->getErrors('attr_err_ini')), 'too big') !== false); - // UPLOAD_ERR_PARTIAL - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(); - $val->validateAttribute($m, 'attr_err_part'); - $this->assertTrue($m->hasErrors('attr_err_part')); - $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); - } + // single File with skipOnEmpty = false + $val = new FileValidator(['skipOnEmpty' => false]); + $m = $this->createModelForAttributeTest(); + $val->validateAttribute($m, 'attr_files'); + $this->assertFalse($m->hasErrors()); + $val->validateAttribute($m, 'attr_files_empty'); + $this->assertTrue($m->hasErrors('attr_files_empty')); + $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); + $m = $this->createModelForAttributeTest(); - public function testValidateAttributeType() - { - $val = new FileValidator(['types' => 'jpeg, jpg']); - $m = FakedValidationModel::createWithAttributes( - [ - 'attr_jpg' => $this->createTestFiles([['name' => 'one.jpeg']]), - 'attr_exe' => $this->createTestFiles([['name' => 'bad.exe']]), - ] - ); - $val->validateAttribute($m, 'attr_jpg'); - $this->assertFalse($m->hasErrors('attr_jpg')); - $val->validateAttribute($m, 'attr_exe'); - $this->assertTrue($m->hasErrors('attr_exe')); - $this->assertTrue(stripos(current($m->getErrors('attr_exe')), 'Only files with these extensions ') !== false); - } + // too big + $val = new FileValidator(['maxSize' => 128]); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'too big') !== false); + // to Small + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(['minSize' => 2048]); + $val->validateAttribute($m, 'attr_files'); + $this->assertTrue($m->hasErrors('attr_files')); + $this->assertTrue(stripos(current($m->getErrors('attr_files')), 'too small') !== false); + // UPLOAD_ERR_INI_SIZE/UPLOAD_ERR_FORM_SIZE + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_ini'); + $this->assertTrue($m->hasErrors('attr_err_ini')); + $this->assertTrue(stripos(current($m->getErrors('attr_err_ini')), 'too big') !== false); + // UPLOAD_ERR_PARTIAL + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_part'); + $this->assertTrue($m->hasErrors('attr_err_part')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); + } + public function testValidateAttributeType() + { + $val = new FileValidator(['types' => 'jpeg, jpg']); + $m = FakedValidationModel::createWithAttributes( + [ + 'attr_jpg' => $this->createTestFiles([['name' => 'one.jpeg']]), + 'attr_exe' => $this->createTestFiles([['name' => 'bad.exe']]), + ] + ); + $val->validateAttribute($m, 'attr_jpg'); + $this->assertFalse($m->hasErrors('attr_jpg')); + $val->validateAttribute($m, 'attr_exe'); + $this->assertTrue($m->hasErrors('attr_exe')); + $this->assertTrue(stripos(current($m->getErrors('attr_exe')), 'Only files with these extensions ') !== false); + } - protected function createModelForAttributeTest() - { - return FakedValidationModel::createWithAttributes( - [ - 'attr_files' => $this->createTestFiles([ - ['name' => 'abc.jpg', 'size' => 1024, 'type' => 'image/jpeg'], - ]), - 'attr_files_empty' => $this->createTestFiles([[]]), - 'attr_err_ini' => $this->createTestFiles([['error' => UPLOAD_ERR_INI_SIZE]]), - 'attr_err_part' => $this->createTestFiles([['error' => UPLOAD_ERR_PARTIAL]]), - 'attr_err_tmp' => $this->createTestFiles([['error' => UPLOAD_ERR_NO_TMP_DIR]]), - 'attr_err_write' => $this->createTestFiles([['error' => UPLOAD_ERR_CANT_WRITE]]), - 'attr_err_ext' => $this->createTestFiles([['error' => UPLOAD_ERR_EXTENSION]]), - ] - ); - } + protected function createModelForAttributeTest() + { + return FakedValidationModel::createWithAttributes( + [ + 'attr_files' => $this->createTestFiles([ + ['name' => 'abc.jpg', 'size' => 1024, 'type' => 'image/jpeg'], + ]), + 'attr_files_empty' => $this->createTestFiles([[]]), + 'attr_err_ini' => $this->createTestFiles([['error' => UPLOAD_ERR_INI_SIZE]]), + 'attr_err_part' => $this->createTestFiles([['error' => UPLOAD_ERR_PARTIAL]]), + 'attr_err_tmp' => $this->createTestFiles([['error' => UPLOAD_ERR_NO_TMP_DIR]]), + 'attr_err_write' => $this->createTestFiles([['error' => UPLOAD_ERR_CANT_WRITE]]), + 'attr_err_ext' => $this->createTestFiles([['error' => UPLOAD_ERR_EXTENSION]]), + ] + ); + } - public function testValidateAttributeErrPartial() - { - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(); - $val->validateAttribute($m, 'attr_err_part'); - $this->assertTrue($m->hasErrors('attr_err_part')); - $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); - } + public function testValidateAttributeErrPartial() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_part'); + $this->assertTrue($m->hasErrors('attr_err_part')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); + } - public function testValidateAttributeErrCantWrite() - { - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(); - $val->validateAttribute($m, 'attr_err_write'); - $this->assertTrue($m->hasErrors('attr_err_write')); - $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_write'))); - } + public function testValidateAttributeErrCantWrite() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_write'); + $this->assertTrue($m->hasErrors('attr_err_write')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_write'))); + } - public function testValidateAttributeErrExtension() - { - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(); - $val->validateAttribute($m, 'attr_err_ext'); - $this->assertTrue($m->hasErrors('attr_err_ext')); - $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_ext'))); - } + public function testValidateAttributeErrExtension() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_ext'); + $this->assertTrue($m->hasErrors('attr_err_ext')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_ext'))); + } - public function testValidateAttributeErrNoTmpDir() - { - $m = $this->createModelForAttributeTest(); - $val = new FileValidator(); - $val->validateAttribute($m, 'attr_err_tmp'); - $this->assertTrue($m->hasErrors('attr_err_tmp')); - $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_tmp'))); - } + public function testValidateAttributeErrNoTmpDir() + { + $m = $this->createModelForAttributeTest(); + $val = new FileValidator(); + $val->validateAttribute($m, 'attr_err_tmp'); + $this->assertTrue($m->hasErrors('attr_err_tmp')); + $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_tmp'))); + } } diff --git a/tests/unit/framework/validators/FilterValidatorTest.php b/tests/unit/framework/validators/FilterValidatorTest.php index e224dcdd066..8a21c47a7ef 100644 --- a/tests/unit/framework/validators/FilterValidatorTest.php +++ b/tests/unit/framework/validators/FilterValidatorTest.php @@ -2,51 +2,50 @@ namespace yiiunit\framework\validators; - use yii\validators\FilterValidator; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; class FilterValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testAssureExceptionOnInit() - { - $this->setExpectedException('yii\base\InvalidConfigException'); - new FilterValidator(); - } + public function testAssureExceptionOnInit() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + new FilterValidator(); + } - public function testValidateAttribute() - { - $m = FakedValidationModel::createWithAttributes([ - 'attr_one' => ' to be trimmed ', - 'attr_two' => 'set this to null', - 'attr_empty1' => '', - 'attr_empty2' => null - ]); - $val = new FilterValidator(['filter' => 'trim']); - $val->validateAttribute($m, 'attr_one'); - $this->assertSame('to be trimmed', $m->attr_one); - $val->filter = function ($value) { - return null; - }; - $val->validateAttribute($m, 'attr_two'); - $this->assertNull($m->attr_two); - $val->filter = [$this, 'notToBeNull']; - $val->validateAttribute($m, 'attr_empty1'); - $this->assertSame($this->notToBeNull(''), $m->attr_empty1); - $val->skipOnEmpty = true; - $val->validateAttribute($m, 'attr_empty2'); - $this->assertNotNull($m->attr_empty2); - } + public function testValidateAttribute() + { + $m = FakedValidationModel::createWithAttributes([ + 'attr_one' => ' to be trimmed ', + 'attr_two' => 'set this to null', + 'attr_empty1' => '', + 'attr_empty2' => null + ]); + $val = new FilterValidator(['filter' => 'trim']); + $val->validateAttribute($m, 'attr_one'); + $this->assertSame('to be trimmed', $m->attr_one); + $val->filter = function ($value) { + return null; + }; + $val->validateAttribute($m, 'attr_two'); + $this->assertNull($m->attr_two); + $val->filter = [$this, 'notToBeNull']; + $val->validateAttribute($m, 'attr_empty1'); + $this->assertSame($this->notToBeNull(''), $m->attr_empty1); + $val->skipOnEmpty = true; + $val->validateAttribute($m, 'attr_empty2'); + $this->assertNotNull($m->attr_empty2); + } - public function notToBeNull($value) - { - return 'not null'; - } + public function notToBeNull($value) + { + return 'not null'; + } } diff --git a/tests/unit/framework/validators/NumberValidatorTest.php b/tests/unit/framework/validators/NumberValidatorTest.php index 0f3384a0ab3..a46809a2613 100644 --- a/tests/unit/framework/validators/NumberValidatorTest.php +++ b/tests/unit/framework/validators/NumberValidatorTest.php @@ -2,165 +2,164 @@ namespace yiiunit\framework\validators; - use yii\validators\NumberValidator; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; class NumberValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testEnsureMessageOnInit() - { - $val = new NumberValidator; - $this->assertTrue(is_string($val->message)); - $this->assertTrue(is_null($val->max)); - $val = new NumberValidator(['min' => -1, 'max' => 20, 'integerOnly' => true]); - $this->assertTrue(is_string($val->message)); - $this->assertTrue(is_string($val->tooSmall)); - $this->assertTrue(is_string($val->tooBig)); - } + public function testEnsureMessageOnInit() + { + $val = new NumberValidator; + $this->assertTrue(is_string($val->message)); + $this->assertTrue(is_null($val->max)); + $val = new NumberValidator(['min' => -1, 'max' => 20, 'integerOnly' => true]); + $this->assertTrue(is_string($val->message)); + $this->assertTrue(is_string($val->tooSmall)); + $this->assertTrue(is_string($val->tooBig)); + } - public function testValidateValueSimple() - { - $val = new NumberValidator(); - $this->assertTrue($val->validate(20)); - $this->assertTrue($val->validate(0)); - $this->assertTrue($val->validate(-20)); - $this->assertTrue($val->validate('20')); - $this->assertTrue($val->validate(25.45)); - $this->assertFalse($val->validate('25,45')); - $this->assertFalse($val->validate('12:45')); - $val = new NumberValidator(['integerOnly' => true]); - $this->assertTrue($val->validate(20)); - $this->assertTrue($val->validate(0)); - $this->assertFalse($val->validate(25.45)); - $this->assertTrue($val->validate('20')); - $this->assertFalse($val->validate('25,45')); - $this->assertTrue($val->validate('020')); - $this->assertTrue($val->validate(0x14)); - $this->assertFalse($val->validate('0x14')); // todo check this - } + public function testValidateValueSimple() + { + $val = new NumberValidator(); + $this->assertTrue($val->validate(20)); + $this->assertTrue($val->validate(0)); + $this->assertTrue($val->validate(-20)); + $this->assertTrue($val->validate('20')); + $this->assertTrue($val->validate(25.45)); + $this->assertFalse($val->validate('25,45')); + $this->assertFalse($val->validate('12:45')); + $val = new NumberValidator(['integerOnly' => true]); + $this->assertTrue($val->validate(20)); + $this->assertTrue($val->validate(0)); + $this->assertFalse($val->validate(25.45)); + $this->assertTrue($val->validate('20')); + $this->assertFalse($val->validate('25,45')); + $this->assertTrue($val->validate('020')); + $this->assertTrue($val->validate(0x14)); + $this->assertFalse($val->validate('0x14')); // todo check this + } - public function testValidateValueAdvanced() - { - $val = new NumberValidator(); - $this->assertTrue($val->validate('-1.23')); // signed float - $this->assertTrue($val->validate('-4.423e-12')); // signed float + exponent - $this->assertTrue($val->validate('12E3')); // integer + exponent - $this->assertFalse($val->validate('e12')); // just exponent - $this->assertFalse($val->validate('-e3')); - $this->assertFalse($val->validate('-4.534-e-12')); // 'signed' exponent - $this->assertFalse($val->validate('12.23^4')); // expression instead of value - $val = new NumberValidator(['integerOnly' => true]); - $this->assertFalse($val->validate('-1.23')); - $this->assertFalse($val->validate('-4.423e-12')); - $this->assertFalse($val->validate('12E3')); - $this->assertFalse($val->validate('e12')); - $this->assertFalse($val->validate('-e3')); - $this->assertFalse($val->validate('-4.534-e-12')); - $this->assertFalse($val->validate('12.23^4')); - } + public function testValidateValueAdvanced() + { + $val = new NumberValidator(); + $this->assertTrue($val->validate('-1.23')); // signed float + $this->assertTrue($val->validate('-4.423e-12')); // signed float + exponent + $this->assertTrue($val->validate('12E3')); // integer + exponent + $this->assertFalse($val->validate('e12')); // just exponent + $this->assertFalse($val->validate('-e3')); + $this->assertFalse($val->validate('-4.534-e-12')); // 'signed' exponent + $this->assertFalse($val->validate('12.23^4')); // expression instead of value + $val = new NumberValidator(['integerOnly' => true]); + $this->assertFalse($val->validate('-1.23')); + $this->assertFalse($val->validate('-4.423e-12')); + $this->assertFalse($val->validate('12E3')); + $this->assertFalse($val->validate('e12')); + $this->assertFalse($val->validate('-e3')); + $this->assertFalse($val->validate('-4.534-e-12')); + $this->assertFalse($val->validate('12.23^4')); + } - public function testValidateValueMin() - { - $val = new NumberValidator(['min' => 1]); - $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(-1)); - $this->assertFalse($val->validate('22e-12')); - $this->assertTrue($val->validate(PHP_INT_MAX + 1)); - $val = new NumberValidator(['min' => 1], ['integerOnly' => true]); - $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(-1)); - $this->assertFalse($val->validate('22e-12')); - $this->assertTrue($val->validate(PHP_INT_MAX + 1)); - } + public function testValidateValueMin() + { + $val = new NumberValidator(['min' => 1]); + $this->assertTrue($val->validate(1)); + $this->assertFalse($val->validate(-1)); + $this->assertFalse($val->validate('22e-12')); + $this->assertTrue($val->validate(PHP_INT_MAX + 1)); + $val = new NumberValidator(['min' => 1], ['integerOnly' => true]); + $this->assertTrue($val->validate(1)); + $this->assertFalse($val->validate(-1)); + $this->assertFalse($val->validate('22e-12')); + $this->assertTrue($val->validate(PHP_INT_MAX + 1)); + } - public function testValidateValueMax() - { - $val = new NumberValidator(['max' => 1.25]); - $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(1.5)); - $this->assertTrue($val->validate('22e-12')); - $this->assertTrue($val->validate('125e-2')); - $val = new NumberValidator(['max' => 1.25, 'integerOnly' => true]); - $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(1.5)); - $this->assertFalse($val->validate('22e-12')); - $this->assertFalse($val->validate('125e-2')); - } + public function testValidateValueMax() + { + $val = new NumberValidator(['max' => 1.25]); + $this->assertTrue($val->validate(1)); + $this->assertFalse($val->validate(1.5)); + $this->assertTrue($val->validate('22e-12')); + $this->assertTrue($val->validate('125e-2')); + $val = new NumberValidator(['max' => 1.25, 'integerOnly' => true]); + $this->assertTrue($val->validate(1)); + $this->assertFalse($val->validate(1.5)); + $this->assertFalse($val->validate('22e-12')); + $this->assertFalse($val->validate('125e-2')); + } - public function testValidateValueRange() - { - $val = new NumberValidator(['min' => -10, 'max' => 20]); - $this->assertTrue($val->validate(0)); - $this->assertTrue($val->validate(-10)); - $this->assertFalse($val->validate(-11)); - $this->assertFalse($val->validate(21)); - $val = new NumberValidator(['min' => -10, 'max' => 20, 'integerOnly' => true]); - $this->assertTrue($val->validate(0)); - $this->assertFalse($val->validate(-11)); - $this->assertFalse($val->validate(22)); - $this->assertFalse($val->validate('20e-1')); - } + public function testValidateValueRange() + { + $val = new NumberValidator(['min' => -10, 'max' => 20]); + $this->assertTrue($val->validate(0)); + $this->assertTrue($val->validate(-10)); + $this->assertFalse($val->validate(-11)); + $this->assertFalse($val->validate(21)); + $val = new NumberValidator(['min' => -10, 'max' => 20, 'integerOnly' => true]); + $this->assertTrue($val->validate(0)); + $this->assertFalse($val->validate(-11)); + $this->assertFalse($val->validate(22)); + $this->assertFalse($val->validate('20e-1')); + } - public function testValidateAttribute() - { - $val = new NumberValidator(); - $model = new FakedValidationModel(); - $model->attr_number = '5.5e1'; - $val->validateAttribute($model, 'attr_number'); - $this->assertFalse($model->hasErrors('attr_number')); - $model->attr_number = '43^32'; //expression - $val->validateAttribute($model, 'attr_number'); - $this->assertTrue($model->hasErrors('attr_number')); - $val = new NumberValidator(['min' => 10]); - $model = new FakedValidationModel(); - $model->attr_number = 10; - $val->validateAttribute($model, 'attr_number'); - $this->assertFalse($model->hasErrors('attr_number')); - $model->attr_number = 5; - $val->validateAttribute($model, 'attr_number'); - $this->assertTrue($model->hasErrors('attr_number')); - $val = new NumberValidator(['max' => 10]); - $model = new FakedValidationModel(); - $model->attr_number = 10; - $val->validateAttribute($model, 'attr_number'); - $this->assertFalse($model->hasErrors('attr_number')); - $model->attr_number = 15; - $val->validateAttribute($model, 'attr_number'); - $this->assertTrue($model->hasErrors('attr_number')); - $val = new NumberValidator(['max' => 10, 'integerOnly' => true]); - $model = new FakedValidationModel(); - $model->attr_number = 10; - $val->validateAttribute($model, 'attr_number'); - $this->assertFalse($model->hasErrors('attr_number')); - $model->attr_number = 3.43; - $val->validateAttribute($model, 'attr_number'); - $this->assertTrue($model->hasErrors('attr_number')); - $val = new NumberValidator(['min' => 1]); - $model = FakedValidationModel::createWithAttributes(['attr_num' => [1, 2, 3]]); - $val->validateAttribute($model, 'attr_num'); - $this->assertTrue($model->hasErrors('attr_num')); - } + public function testValidateAttribute() + { + $val = new NumberValidator(); + $model = new FakedValidationModel(); + $model->attr_number = '5.5e1'; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = '43^32'; //expression + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(['min' => 10]); + $model = new FakedValidationModel(); + $model->attr_number = 10; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = 5; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(['max' => 10]); + $model = new FakedValidationModel(); + $model->attr_number = 10; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = 15; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(['max' => 10, 'integerOnly' => true]); + $model = new FakedValidationModel(); + $model->attr_number = 10; + $val->validateAttribute($model, 'attr_number'); + $this->assertFalse($model->hasErrors('attr_number')); + $model->attr_number = 3.43; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $val = new NumberValidator(['min' => 1]); + $model = FakedValidationModel::createWithAttributes(['attr_num' => [1, 2, 3]]); + $val->validateAttribute($model, 'attr_num'); + $this->assertTrue($model->hasErrors('attr_num')); + } - public function testEnsureCustomMessageIsSetOnValidateAttribute() - { - $val = new NumberValidator([ - 'tooSmall' => '{attribute} is to small.', - 'min' => 5 - ]); - $model = new FakedValidationModel(); - $model->attr_number = 0; - $val->validateAttribute($model, 'attr_number'); - $this->assertTrue($model->hasErrors('attr_number')); - $this->assertEquals(1, count($model->getErrors('attr_number'))); - $msgs = $model->getErrors('attr_number'); - $this->assertSame('attr_number is to small.', $msgs[0]); - } + public function testEnsureCustomMessageIsSetOnValidateAttribute() + { + $val = new NumberValidator([ + 'tooSmall' => '{attribute} is to small.', + 'min' => 5 + ]); + $model = new FakedValidationModel(); + $model->attr_number = 0; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $this->assertEquals(1, count($model->getErrors('attr_number'))); + $msgs = $model->getErrors('attr_number'); + $this->assertSame('attr_number is to small.', $msgs[0]); + } } diff --git a/tests/unit/framework/validators/RangeValidatorTest.php b/tests/unit/framework/validators/RangeValidatorTest.php index ddbadb593a1..0c27412a1c5 100644 --- a/tests/unit/framework/validators/RangeValidatorTest.php +++ b/tests/unit/framework/validators/RangeValidatorTest.php @@ -2,75 +2,74 @@ namespace yiiunit\framework\validators; - use yii\validators\RangeValidator; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; class RangeValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testInitException() - { - $this->setExpectedException('yii\base\InvalidConfigException', 'The "range" property must be set.'); - new RangeValidator(['range' => 'not an array']); - } + public function testInitException() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'The "range" property must be set.'); + new RangeValidator(['range' => 'not an array']); + } - public function testAssureMessageSetOnInit() - { - $val = new RangeValidator(['range' => []]); - $this->assertTrue(is_string($val->message)); - } + public function testAssureMessageSetOnInit() + { + $val = new RangeValidator(['range' => []]); + $this->assertTrue(is_string($val->message)); + } - public function testValidateValue() - { - $val = new RangeValidator(['range' => range(1, 10, 1)]); - $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(0)); - $this->assertFalse($val->validate(11)); - $this->assertFalse($val->validate(5.5)); - $this->assertTrue($val->validate(10)); - $this->assertTrue($val->validate("10")); - $this->assertTrue($val->validate("5")); - } + public function testValidateValue() + { + $val = new RangeValidator(['range' => range(1, 10, 1)]); + $this->assertTrue($val->validate(1)); + $this->assertFalse($val->validate(0)); + $this->assertFalse($val->validate(11)); + $this->assertFalse($val->validate(5.5)); + $this->assertTrue($val->validate(10)); + $this->assertTrue($val->validate("10")); + $this->assertTrue($val->validate("5")); + } - public function testValidateValueStrict() - { - $val = new RangeValidator(['range' => range(1, 10, 1), 'strict' => true]); - $this->assertTrue($val->validate(1)); - $this->assertTrue($val->validate(5)); - $this->assertTrue($val->validate(10)); - $this->assertFalse($val->validate("1")); - $this->assertFalse($val->validate("10")); - $this->assertFalse($val->validate("5.5")); - } + public function testValidateValueStrict() + { + $val = new RangeValidator(['range' => range(1, 10, 1), 'strict' => true]); + $this->assertTrue($val->validate(1)); + $this->assertTrue($val->validate(5)); + $this->assertTrue($val->validate(10)); + $this->assertFalse($val->validate("1")); + $this->assertFalse($val->validate("10")); + $this->assertFalse($val->validate("5.5")); + } - public function testValidateValueNot() - { - $val = new RangeValidator(['range' => range(1, 10, 1), 'not' => true]); - $this->assertFalse($val->validate(1)); - $this->assertTrue($val->validate(0)); - $this->assertTrue($val->validate(11)); - $this->assertTrue($val->validate(5.5)); - $this->assertFalse($val->validate(10)); - $this->assertFalse($val->validate("10")); - $this->assertFalse($val->validate("5")); - } + public function testValidateValueNot() + { + $val = new RangeValidator(['range' => range(1, 10, 1), 'not' => true]); + $this->assertFalse($val->validate(1)); + $this->assertTrue($val->validate(0)); + $this->assertTrue($val->validate(11)); + $this->assertTrue($val->validate(5.5)); + $this->assertFalse($val->validate(10)); + $this->assertFalse($val->validate("10")); + $this->assertFalse($val->validate("5")); + } - public function testValidateAttribute() - { - $val = new RangeValidator(['range' => range(1, 10, 1)]); - $m = FakedValidationModel::createWithAttributes(['attr_r1' => 5, 'attr_r2' => 999]); - $val->validateAttribute($m, 'attr_r1'); - $this->assertFalse($m->hasErrors()); - $val->validateAttribute($m, 'attr_r2'); - $this->assertTrue($m->hasErrors('attr_r2')); - $err = $m->getErrors('attr_r2'); - $this->assertTrue(stripos($err[0], 'attr_r2') !== false); - } + public function testValidateAttribute() + { + $val = new RangeValidator(['range' => range(1, 10, 1)]); + $m = FakedValidationModel::createWithAttributes(['attr_r1' => 5, 'attr_r2' => 999]); + $val->validateAttribute($m, 'attr_r1'); + $this->assertFalse($m->hasErrors()); + $val->validateAttribute($m, 'attr_r2'); + $this->assertTrue($m->hasErrors('attr_r2')); + $err = $m->getErrors('attr_r2'); + $this->assertTrue(stripos($err[0], 'attr_r2') !== false); + } } diff --git a/tests/unit/framework/validators/RegularExpressionValidatorTest.php b/tests/unit/framework/validators/RegularExpressionValidatorTest.php index 9905ba5320d..c6226da327d 100644 --- a/tests/unit/framework/validators/RegularExpressionValidatorTest.php +++ b/tests/unit/framework/validators/RegularExpressionValidatorTest.php @@ -2,52 +2,51 @@ namespace yiiunit\framework\validators; - use yii\validators\RegularExpressionValidator; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; class RegularExpressionValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - public function testValidateValue() - { - $val = new RegularExpressionValidator(['pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m']); - $this->assertTrue($val->validate('b.4')); - $this->assertFalse($val->validate('b./')); - $this->assertFalse($val->validate(['a', 'b'])); - $val->not = true; - $this->assertFalse($val->validate('b.4')); - $this->assertTrue($val->validate('b./')); - $this->assertFalse($val->validate(['a', 'b'])); - } - - public function testValidateAttribute() - { - $val = new RegularExpressionValidator(['pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m']); - $m = FakedValidationModel::createWithAttributes(['attr_reg1' => 'b.4']); - $val->validateAttribute($m, 'attr_reg1'); - $this->assertFalse($m->hasErrors('attr_reg1')); - $m->attr_reg1 = 'b./'; - $val->validateAttribute($m, 'attr_reg1'); - $this->assertTrue($m->hasErrors('attr_reg1')); - } - - public function testMessageSetOnInit() - { - $val = new RegularExpressionValidator(['pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m']); - $this->assertTrue(is_string($val->message)); - } - - public function testInitException() - { - $this->setExpectedException('yii\base\InvalidConfigException'); - $val = new RegularExpressionValidator(); - $val->validate('abc'); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + public function testValidateValue() + { + $val = new RegularExpressionValidator(['pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m']); + $this->assertTrue($val->validate('b.4')); + $this->assertFalse($val->validate('b./')); + $this->assertFalse($val->validate(['a', 'b'])); + $val->not = true; + $this->assertFalse($val->validate('b.4')); + $this->assertTrue($val->validate('b./')); + $this->assertFalse($val->validate(['a', 'b'])); + } + + public function testValidateAttribute() + { + $val = new RegularExpressionValidator(['pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m']); + $m = FakedValidationModel::createWithAttributes(['attr_reg1' => 'b.4']); + $val->validateAttribute($m, 'attr_reg1'); + $this->assertFalse($m->hasErrors('attr_reg1')); + $m->attr_reg1 = 'b./'; + $val->validateAttribute($m, 'attr_reg1'); + $this->assertTrue($m->hasErrors('attr_reg1')); + } + + public function testMessageSetOnInit() + { + $val = new RegularExpressionValidator(['pattern' => '/^[a-zA-Z0-9](\.)?([^\/]*)$/m']); + $this->assertTrue(is_string($val->message)); + } + + public function testInitException() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + $val = new RegularExpressionValidator(); + $val->validate('abc'); + } } diff --git a/tests/unit/framework/validators/RequiredValidatorTest.php b/tests/unit/framework/validators/RequiredValidatorTest.php index 44102eb280e..2e892d614eb 100644 --- a/tests/unit/framework/validators/RequiredValidatorTest.php +++ b/tests/unit/framework/validators/RequiredValidatorTest.php @@ -1,60 +1,59 @@ mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateValueWithDefaults() - { - $val = new RequiredValidator(); - $this->assertFalse($val->validate(null)); - $this->assertFalse($val->validate([])); - $this->assertTrue($val->validate('not empty')); - $this->assertTrue($val->validate(['with', 'elements'])); - } + public function testValidateValueWithDefaults() + { + $val = new RequiredValidator(); + $this->assertFalse($val->validate(null)); + $this->assertFalse($val->validate([])); + $this->assertTrue($val->validate('not empty')); + $this->assertTrue($val->validate(['with', 'elements'])); + } - public function testValidateValueWithValue() - { - $val = new RequiredValidator(['requiredValue' => 55]); - $this->assertTrue($val->validate(55)); - $this->assertTrue($val->validate("55")); - $this->assertTrue($val->validate("0x37")); - $this->assertFalse($val->validate("should fail")); - $this->assertTrue($val->validate(true)); - $val->strict = true; - $this->assertTrue($val->validate(55)); - $this->assertFalse($val->validate("55")); - $this->assertFalse($val->validate("0x37")); - $this->assertFalse($val->validate("should fail")); - $this->assertFalse($val->validate(true)); - } + public function testValidateValueWithValue() + { + $val = new RequiredValidator(['requiredValue' => 55]); + $this->assertTrue($val->validate(55)); + $this->assertTrue($val->validate("55")); + $this->assertTrue($val->validate("0x37")); + $this->assertFalse($val->validate("should fail")); + $this->assertTrue($val->validate(true)); + $val->strict = true; + $this->assertTrue($val->validate(55)); + $this->assertFalse($val->validate("55")); + $this->assertFalse($val->validate("0x37")); + $this->assertFalse($val->validate("should fail")); + $this->assertFalse($val->validate(true)); + } - public function testValidateAttribute() - { - // empty req-value - $val = new RequiredValidator(); - $m = FakedValidationModel::createWithAttributes(['attr_val' => null]); - $val->validateAttribute($m, 'attr_val'); - $this->assertTrue($m->hasErrors('attr_val')); - $this->assertTrue(stripos(current($m->getErrors('attr_val')), 'blank') !== false); - $val = new RequiredValidator(['requiredValue' => 55]); - $m = FakedValidationModel::createWithAttributes(['attr_val' => 56]); - $val->validateAttribute($m, 'attr_val'); - $this->assertTrue($m->hasErrors('attr_val')); - $this->assertTrue(stripos(current($m->getErrors('attr_val')), 'must be') !== false); - $val = new RequiredValidator(['requiredValue' => 55]); - $m = FakedValidationModel::createWithAttributes(['attr_val' => 55]); - $val->validateAttribute($m, 'attr_val'); - $this->assertFalse($m->hasErrors('attr_val')); - } + public function testValidateAttribute() + { + // empty req-value + $val = new RequiredValidator(); + $m = FakedValidationModel::createWithAttributes(['attr_val' => null]); + $val->validateAttribute($m, 'attr_val'); + $this->assertTrue($m->hasErrors('attr_val')); + $this->assertTrue(stripos(current($m->getErrors('attr_val')), 'blank') !== false); + $val = new RequiredValidator(['requiredValue' => 55]); + $m = FakedValidationModel::createWithAttributes(['attr_val' => 56]); + $val->validateAttribute($m, 'attr_val'); + $this->assertTrue($m->hasErrors('attr_val')); + $this->assertTrue(stripos(current($m->getErrors('attr_val')), 'must be') !== false); + $val = new RequiredValidator(['requiredValue' => 55]); + $m = FakedValidationModel::createWithAttributes(['attr_val' => 55]); + $val->validateAttribute($m, 'attr_val'); + $this->assertFalse($m->hasErrors('attr_val')); + } } diff --git a/tests/unit/framework/validators/StringValidatorTest.php b/tests/unit/framework/validators/StringValidatorTest.php index 50ebbbb1e5e..c7afc78a752 100644 --- a/tests/unit/framework/validators/StringValidatorTest.php +++ b/tests/unit/framework/validators/StringValidatorTest.php @@ -2,115 +2,114 @@ namespace yiiunit\framework\validators; - use yii\validators\StringValidator; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; class StringValidatorTest extends TestCase { - public function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + public function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateValue() - { - $val = new StringValidator(); - $this->assertFalse($val->validate(['not a string'])); - $this->assertTrue($val->validate('Just some string')); - } + public function testValidateValue() + { + $val = new StringValidator(); + $this->assertFalse($val->validate(['not a string'])); + $this->assertTrue($val->validate('Just some string')); + } - public function testValidateValueLength() - { - $val = new StringValidator(['length' => 25]); - $this->assertTrue($val->validate(str_repeat('x', 25))); - $this->assertTrue($val->validate(str_repeat('€', 25))); - $this->assertFalse($val->validate(str_repeat('x', 125))); - $this->assertFalse($val->validate('')); - $val = new StringValidator(['length' => [25]]); - $this->assertTrue($val->validate(str_repeat('x', 25))); - $this->assertTrue($val->validate(str_repeat('x', 1250))); - $this->assertFalse($val->validate(str_repeat('Ä', 24))); - $this->assertFalse($val->validate('')); - $val = new StringValidator(['length' => [10, 20]]); - $this->assertTrue($val->validate(str_repeat('x', 15))); - $this->assertTrue($val->validate(str_repeat('x', 10))); - $this->assertTrue($val->validate(str_repeat('x', 20))); - $this->assertFalse($val->validate(str_repeat('x', 5))); - $this->assertFalse($val->validate(str_repeat('x', 25))); - $this->assertFalse($val->validate('')); - // make sure min/max are overridden - $val = new StringValidator(['length' => [10, 20], 'min' => 25, 'max' => 35]); - $this->assertTrue($val->validate(str_repeat('x', 15))); - $this->assertFalse($val->validate(str_repeat('x', 30))); - } + public function testValidateValueLength() + { + $val = new StringValidator(['length' => 25]); + $this->assertTrue($val->validate(str_repeat('x', 25))); + $this->assertTrue($val->validate(str_repeat('€', 25))); + $this->assertFalse($val->validate(str_repeat('x', 125))); + $this->assertFalse($val->validate('')); + $val = new StringValidator(['length' => [25]]); + $this->assertTrue($val->validate(str_repeat('x', 25))); + $this->assertTrue($val->validate(str_repeat('x', 1250))); + $this->assertFalse($val->validate(str_repeat('Ä', 24))); + $this->assertFalse($val->validate('')); + $val = new StringValidator(['length' => [10, 20]]); + $this->assertTrue($val->validate(str_repeat('x', 15))); + $this->assertTrue($val->validate(str_repeat('x', 10))); + $this->assertTrue($val->validate(str_repeat('x', 20))); + $this->assertFalse($val->validate(str_repeat('x', 5))); + $this->assertFalse($val->validate(str_repeat('x', 25))); + $this->assertFalse($val->validate('')); + // make sure min/max are overridden + $val = new StringValidator(['length' => [10, 20], 'min' => 25, 'max' => 35]); + $this->assertTrue($val->validate(str_repeat('x', 15))); + $this->assertFalse($val->validate(str_repeat('x', 30))); + } - public function testValidateValueMinMax() - { - $val = new StringValidator(['min' => 10]); - $this->assertTrue($val->validate(str_repeat('x', 10))); - $this->assertFalse($val->validate('xxxx')); - $val = new StringValidator(['max' => 10]); - $this->assertTrue($val->validate('xxxx')); - $this->assertFalse($val->validate(str_repeat('y', 20))); - $val = new StringValidator(['min' => 10, 'max' => 20]); - $this->assertTrue($val->validate(str_repeat('y', 15))); - $this->assertFalse($val->validate('abc')); - $this->assertFalse($val->validate(str_repeat('b', 25))); - } + public function testValidateValueMinMax() + { + $val = new StringValidator(['min' => 10]); + $this->assertTrue($val->validate(str_repeat('x', 10))); + $this->assertFalse($val->validate('xxxx')); + $val = new StringValidator(['max' => 10]); + $this->assertTrue($val->validate('xxxx')); + $this->assertFalse($val->validate(str_repeat('y', 20))); + $val = new StringValidator(['min' => 10, 'max' => 20]); + $this->assertTrue($val->validate(str_repeat('y', 15))); + $this->assertFalse($val->validate('abc')); + $this->assertFalse($val->validate(str_repeat('b', 25))); + } - public function testValidateAttribute() - { - $val = new StringValidator(); - $model = new FakedValidationModel(); - $model->attr_string = 'a tet string'; - $val->validateAttribute($model, 'attr_string'); - $this->assertFalse($model->hasErrors()); - $val = new StringValidator(['length' => 20]); - $model = new FakedValidationModel(); - $model->attr_string = str_repeat('x', 20); - $val->validateAttribute($model, 'attr_string'); - $this->assertFalse($model->hasErrors()); - $model = new FakedValidationModel(); - $model->attr_string = 'abc'; - $val->validateAttribute($model, 'attr_string'); - $this->assertTrue($model->hasErrors('attr_string')); - $val = new StringValidator(['max' => 2]); - $model = new FakedValidationModel(); - $model->attr_string = 'a'; - $val->validateAttribute($model, 'attr_string'); - $this->assertFalse($model->hasErrors()); - $model = new FakedValidationModel(); - $model->attr_string = 'abc'; - $val->validateAttribute($model, 'attr_string'); - $this->assertTrue($model->hasErrors('attr_string')); - $val = new StringValidator(['max' => 1]); - $model = FakedValidationModel::createWithAttributes(['attr_str' => ['abc']]); - $val->validateAttribute($model, 'attr_str'); - $this->assertTrue($model->hasErrors('attr_str')); - } + public function testValidateAttribute() + { + $val = new StringValidator(); + $model = new FakedValidationModel(); + $model->attr_string = 'a tet string'; + $val->validateAttribute($model, 'attr_string'); + $this->assertFalse($model->hasErrors()); + $val = new StringValidator(['length' => 20]); + $model = new FakedValidationModel(); + $model->attr_string = str_repeat('x', 20); + $val->validateAttribute($model, 'attr_string'); + $this->assertFalse($model->hasErrors()); + $model = new FakedValidationModel(); + $model->attr_string = 'abc'; + $val->validateAttribute($model, 'attr_string'); + $this->assertTrue($model->hasErrors('attr_string')); + $val = new StringValidator(['max' => 2]); + $model = new FakedValidationModel(); + $model->attr_string = 'a'; + $val->validateAttribute($model, 'attr_string'); + $this->assertFalse($model->hasErrors()); + $model = new FakedValidationModel(); + $model->attr_string = 'abc'; + $val->validateAttribute($model, 'attr_string'); + $this->assertTrue($model->hasErrors('attr_string')); + $val = new StringValidator(['max' => 1]); + $model = FakedValidationModel::createWithAttributes(['attr_str' => ['abc']]); + $val->validateAttribute($model, 'attr_str'); + $this->assertTrue($model->hasErrors('attr_str')); + } - public function testEnsureMessagesOnInit() - { - $val = new StringValidator(['min' => 1, 'max' => 2]); - $this->assertTrue(is_string($val->message)); - $this->assertTrue(is_string($val->tooLong)); - $this->assertTrue(is_string($val->tooShort)); - } + public function testEnsureMessagesOnInit() + { + $val = new StringValidator(['min' => 1, 'max' => 2]); + $this->assertTrue(is_string($val->message)); + $this->assertTrue(is_string($val->tooLong)); + $this->assertTrue(is_string($val->tooShort)); + } - public function testCustomErrorMessageInValidateAttribute() - { - $val = new StringValidator([ - 'min' => 5, - 'tooShort' => '{attribute} to short. Min is {min}', - ]); - $model = new FakedValidationModel(); - $model->attr_string = 'abc'; - $val->validateAttribute($model, 'attr_string'); - $this->assertTrue($model->hasErrors('attr_string')); - $errorMsg = $model->getErrors('attr_string'); - $this->assertEquals('attr_string to short. Min is 5', $errorMsg[0]); - } + public function testCustomErrorMessageInValidateAttribute() + { + $val = new StringValidator([ + 'min' => 5, + 'tooShort' => '{attribute} to short. Min is {min}', + ]); + $model = new FakedValidationModel(); + $model->attr_string = 'abc'; + $val->validateAttribute($model, 'attr_string'); + $this->assertTrue($model->hasErrors('attr_string')); + $errorMsg = $model->getErrors('attr_string'); + $this->assertEquals('attr_string to short. Min is 5', $errorMsg[0]); + } } diff --git a/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php index 6b8d100c4c6..64a663024a3 100644 --- a/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php +++ b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorPostgresTest.php @@ -2,10 +2,9 @@ namespace yiiunit\framework\validators\UniqueValidatorDriverTests; - use yiiunit\framework\validators\UniqueValidatorTest; class UniqueValidatorPostgresTest extends UniqueValidatorTest { - protected $driverName = 'pgsql'; + protected $driverName = 'pgsql'; } diff --git a/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php index 10506773d54..a2918530410 100644 --- a/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php +++ b/tests/unit/framework/validators/UniqueValidatorDriverTests/UniqueValidatorSQliteTest.php @@ -2,10 +2,9 @@ namespace yiiunit\framework\validators\UniqueValidatorDriverTests; - use yiiunit\framework\validators\UniqueValidatorTest; class UniqueValidatorSQliteTest extends UniqueValidatorTest { - protected $driverName = 'sqlite'; + protected $driverName = 'sqlite'; } diff --git a/tests/unit/framework/validators/UniqueValidatorTest.php b/tests/unit/framework/validators/UniqueValidatorTest.php index 4af3d29a373..162a0ac7f70 100644 --- a/tests/unit/framework/validators/UniqueValidatorTest.php +++ b/tests/unit/framework/validators/UniqueValidatorTest.php @@ -2,7 +2,6 @@ namespace yiiunit\framework\validators; - use yii\validators\UniqueValidator; use Yii; use yiiunit\data\ar\ActiveRecord; @@ -15,123 +14,123 @@ class UniqueValidatorTest extends DatabaseTestCase { - protected $driverName = 'mysql'; + protected $driverName = 'mysql'; - public function setUp() - { - parent::setUp(); - $this->mockApplication(); - ActiveRecord::$db = $this->getConnection(); - } + public function setUp() + { + parent::setUp(); + $this->mockApplication(); + ActiveRecord::$db = $this->getConnection(); + } - public function testAssureMessageSetOnInit() - { - $val = new UniqueValidator(); - $this->assertTrue(is_string($val->message)); - } + public function testAssureMessageSetOnInit() + { + $val = new UniqueValidator(); + $this->assertTrue(is_string($val->message)); + } - public function testValidateAttributeDefault() - { - $val = new UniqueValidator(); - $m = ValidatorTestMainModel::find()->one(); - $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); - $m = ValidatorTestRefModel::find(1); - $val->validateAttribute($m, 'ref'); - $this->assertTrue($m->hasErrors('ref')); - // new record: - $m = new ValidatorTestRefModel(); - $m->ref = 5; - $val->validateAttribute($m, 'ref'); - $this->assertTrue($m->hasErrors('ref')); - $m = new ValidatorTestRefModel(); - $m->id = 7; - $m->ref = 12121; - $val->validateAttribute($m, 'ref'); - $this->assertFalse($m->hasErrors('ref')); - $m->save(false); - $val->validateAttribute($m, 'ref'); - $this->assertFalse($m->hasErrors('ref')); - // array error - $m = FakedValidationModel::createWithAttributes(['attr_arr' => ['a', 'b']]); - $val->validateAttribute($m, 'attr_arr'); - $this->assertTrue($m->hasErrors('attr_arr')); - } + public function testValidateAttributeDefault() + { + $val = new UniqueValidator(); + $m = ValidatorTestMainModel::find()->one(); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m = ValidatorTestRefModel::find(1); + $val->validateAttribute($m, 'ref'); + $this->assertTrue($m->hasErrors('ref')); + // new record: + $m = new ValidatorTestRefModel(); + $m->ref = 5; + $val->validateAttribute($m, 'ref'); + $this->assertTrue($m->hasErrors('ref')); + $m = new ValidatorTestRefModel(); + $m->id = 7; + $m->ref = 12121; + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors('ref')); + $m->save(false); + $val->validateAttribute($m, 'ref'); + $this->assertFalse($m->hasErrors('ref')); + // array error + $m = FakedValidationModel::createWithAttributes(['attr_arr' => ['a', 'b']]); + $val->validateAttribute($m, 'attr_arr'); + $this->assertTrue($m->hasErrors('attr_arr')); + } - public function testValidateAttributeOfNonARModel() - { - $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']); - $m = FakedValidationModel::createWithAttributes(['attr_1' => 5, 'attr_2' => 1313]); - $val->validateAttribute($m, 'attr_1'); - $this->assertTrue($m->hasErrors('attr_1')); - $val->validateAttribute($m, 'attr_2'); - $this->assertFalse($m->hasErrors('attr_2')); - } + public function testValidateAttributeOfNonARModel() + { + $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']); + $m = FakedValidationModel::createWithAttributes(['attr_1' => 5, 'attr_2' => 1313]); + $val->validateAttribute($m, 'attr_1'); + $this->assertTrue($m->hasErrors('attr_1')); + $val->validateAttribute($m, 'attr_2'); + $this->assertFalse($m->hasErrors('attr_2')); + } - public function testValidateNonDatabaseAttribute() - { - $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']); - $m = ValidatorTestMainModel::find(1); - $val->validateAttribute($m, 'testMainVal'); - $this->assertFalse($m->hasErrors('testMainVal')); - $m = ValidatorTestMainModel::find(1); - $m->testMainVal = 4; - $val->validateAttribute($m, 'testMainVal'); - $this->assertTrue($m->hasErrors('testMainVal')); - } + public function testValidateNonDatabaseAttribute() + { + $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']); + $m = ValidatorTestMainModel::find(1); + $val->validateAttribute($m, 'testMainVal'); + $this->assertFalse($m->hasErrors('testMainVal')); + $m = ValidatorTestMainModel::find(1); + $m->testMainVal = 4; + $val->validateAttribute($m, 'testMainVal'); + $this->assertTrue($m->hasErrors('testMainVal')); + } - public function testValidateAttributeAttributeNotInTableException() - { - $this->setExpectedException('yii\db\Exception'); - $val = new UniqueValidator(); - $m = new ValidatorTestMainModel(); - $val->validateAttribute($m, 'testMainVal'); - } + public function testValidateAttributeAttributeNotInTableException() + { + $this->setExpectedException('yii\db\Exception'); + $val = new UniqueValidator(); + $m = new ValidatorTestMainModel(); + $val->validateAttribute($m, 'testMainVal'); + } - public function testValidateCompositeKeys() - { - $val = new UniqueValidator([ - 'targetClass' => OrderItem::className(), - 'targetAttribute' => ['order_id', 'item_id'], - ]); - // validate old record - $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); - $val->validateAttribute($m, 'order_id'); - $this->assertFalse($m->hasErrors('order_id')); - $m->item_id = 1; - $val->validateAttribute($m, 'order_id'); - $this->assertTrue($m->hasErrors('order_id')); + public function testValidateCompositeKeys() + { + $val = new UniqueValidator([ + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['order_id', 'item_id'], + ]); + // validate old record + $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); + $m->item_id = 1; + $val->validateAttribute($m, 'order_id'); + $this->assertTrue($m->hasErrors('order_id')); - // validate new record - $m = new OrderItem(['order_id' => 1, 'item_id' => 2]); - $val->validateAttribute($m, 'order_id'); - $this->assertTrue($m->hasErrors('order_id')); - $m = new OrderItem(['order_id' => 10, 'item_id' => 2]); - $val->validateAttribute($m, 'order_id'); - $this->assertFalse($m->hasErrors('order_id')); + // validate new record + $m = new OrderItem(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertTrue($m->hasErrors('order_id')); + $m = new OrderItem(['order_id' => 10, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); - $val = new UniqueValidator([ - 'targetClass' => OrderItem::className(), - 'targetAttribute' => ['id' => 'order_id'], - ]); - // validate old record - $m = Order::find(1); - $val->validateAttribute($m, 'id'); - $this->assertTrue($m->hasErrors('id')); - $m = Order::find(1); - $m->id = 2; - $val->validateAttribute($m, 'id'); - $this->assertTrue($m->hasErrors('id')); - $m = Order::find(1); - $m->id = 10; - $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); + $val = new UniqueValidator([ + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['id' => 'order_id'], + ]); + // validate old record + $m = Order::find(1); + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + $m = Order::find(1); + $m->id = 2; + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + $m = Order::find(1); + $m->id = 10; + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); - $m = new Order(['id' => 1]); - $val->validateAttribute($m, 'id'); - $this->assertTrue($m->hasErrors('id')); - $m = new Order(['id' => 10]); - $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); - } + $m = new Order(['id' => 1]); + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + $m = new Order(['id' => 10]); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + } } diff --git a/tests/unit/framework/validators/UrlValidatorTest.php b/tests/unit/framework/validators/UrlValidatorTest.php index 3d99235987d..74cc3e89ab0 100644 --- a/tests/unit/framework/validators/UrlValidatorTest.php +++ b/tests/unit/framework/validators/UrlValidatorTest.php @@ -11,92 +11,93 @@ */ class UrlValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testValidateValue() - { - $val = new UrlValidator; - $this->assertFalse($val->validate('google.de')); - $this->assertTrue($val->validate('http://google.de')); - $this->assertTrue($val->validate('https://google.de')); - $this->assertFalse($val->validate('htp://yiiframework.com')); - $this->assertTrue($val->validate('https://www.google.de/search?q=yii+framework&ie=utf-8&oe=utf-8' - .'&rls=org.mozilla:de:official&client=firefox-a&gws_rd=cr')); - $this->assertFalse($val->validate('ftp://ftp.ruhr-uni-bochum.de/')); - $this->assertFalse($val->validate('http://invalid,domain')); - $this->assertFalse($val->validate('http://äüö?=!"§$%&/()=}][{³²€.edu')); - } - - public function testValidateValueWithDefaultScheme() - { - $val = new UrlValidator(['defaultScheme' => 'https']); - $this->assertTrue($val->validate('yiiframework.com')); - $this->assertTrue($val->validate('http://yiiframework.com')); - } + public function testValidateValue() + { + $val = new UrlValidator; + $this->assertFalse($val->validate('google.de')); + $this->assertTrue($val->validate('http://google.de')); + $this->assertTrue($val->validate('https://google.de')); + $this->assertFalse($val->validate('htp://yiiframework.com')); + $this->assertTrue($val->validate('https://www.google.de/search?q=yii+framework&ie=utf-8&oe=utf-8' + .'&rls=org.mozilla:de:official&client=firefox-a&gws_rd=cr')); + $this->assertFalse($val->validate('ftp://ftp.ruhr-uni-bochum.de/')); + $this->assertFalse($val->validate('http://invalid,domain')); + $this->assertFalse($val->validate('http://äüö?=!"§$%&/()=}][{³²€.edu')); + } - public function testValidateValueWithoutScheme() - { - $val = new UrlValidator(['pattern' => '/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)/i']); - $this->assertTrue($val->validate('yiiframework.com')); - } - - public function testValidateWithCustomScheme() - { - $val = new UrlValidator([ - 'validSchemes' => ['http', 'https', 'ftp', 'ftps'], - 'defaultScheme' => 'http', - ]); - $this->assertTrue($val->validate('ftp://ftp.ruhr-uni-bochum.de/')); - $this->assertTrue($val->validate('google.de')); - $this->assertTrue($val->validate('http://google.de')); - $this->assertTrue($val->validate('https://google.de')); - $this->assertFalse($val->validate('htp://yiiframework.com')); - // relative urls not supported - $this->assertFalse($val->validate('//yiiframework.com')); - } - - public function testValidateWithIdn() - { - if (!function_exists('idn_to_ascii')) { - $this->markTestSkipped('intl package required'); - return; - } - $val = new UrlValidator([ - 'enableIDN' => true, - ]); - $this->assertTrue($val->validate('http://äüößìà.de')); - // converted via http://mct.verisign-grs.com/convertServlet - $this->assertTrue($val->validate('http://xn--zcack7ayc9a.de')); - } - - public function testValidateLength() - { - $url = 'http://' . str_pad('base', 2000, 'url') . '.de'; - $val = new UrlValidator; - $this->assertFalse($val->validate($url)); - } - - public function testValidateAttributeAndError() - { - $obj = new FakedValidationModel; - $obj->attr_url = 'http://google.de'; - $val = new UrlValidator; - $val->validateAttribute($obj, 'attr_url'); - $this->assertFalse($obj->hasErrors('attr_url')); - $this->assertSame('http://google.de', $obj->attr_url); - $obj = new FakedValidationModel; - $val->defaultScheme = 'http'; - $obj->attr_url = 'google.de'; - $val->validateAttribute($obj, 'attr_url'); - $this->assertFalse($obj->hasErrors('attr_url')); - $this->assertTrue(stripos($obj->attr_url, 'http') !== false); - $obj = new FakedValidationModel; - $obj->attr_url = 'gttp;/invalid string'; - $val->validateAttribute($obj, 'attr_url'); - $this->assertTrue($obj->hasErrors('attr_url')); - } + public function testValidateValueWithDefaultScheme() + { + $val = new UrlValidator(['defaultScheme' => 'https']); + $this->assertTrue($val->validate('yiiframework.com')); + $this->assertTrue($val->validate('http://yiiframework.com')); + } + + public function testValidateValueWithoutScheme() + { + $val = new UrlValidator(['pattern' => '/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)/i']); + $this->assertTrue($val->validate('yiiframework.com')); + } + + public function testValidateWithCustomScheme() + { + $val = new UrlValidator([ + 'validSchemes' => ['http', 'https', 'ftp', 'ftps'], + 'defaultScheme' => 'http', + ]); + $this->assertTrue($val->validate('ftp://ftp.ruhr-uni-bochum.de/')); + $this->assertTrue($val->validate('google.de')); + $this->assertTrue($val->validate('http://google.de')); + $this->assertTrue($val->validate('https://google.de')); + $this->assertFalse($val->validate('htp://yiiframework.com')); + // relative urls not supported + $this->assertFalse($val->validate('//yiiframework.com')); + } + + public function testValidateWithIdn() + { + if (!function_exists('idn_to_ascii')) { + $this->markTestSkipped('intl package required'); + + return; + } + $val = new UrlValidator([ + 'enableIDN' => true, + ]); + $this->assertTrue($val->validate('http://äüößìà.de')); + // converted via http://mct.verisign-grs.com/convertServlet + $this->assertTrue($val->validate('http://xn--zcack7ayc9a.de')); + } + + public function testValidateLength() + { + $url = 'http://' . str_pad('base', 2000, 'url') . '.de'; + $val = new UrlValidator; + $this->assertFalse($val->validate($url)); + } + + public function testValidateAttributeAndError() + { + $obj = new FakedValidationModel; + $obj->attr_url = 'http://google.de'; + $val = new UrlValidator; + $val->validateAttribute($obj, 'attr_url'); + $this->assertFalse($obj->hasErrors('attr_url')); + $this->assertSame('http://google.de', $obj->attr_url); + $obj = new FakedValidationModel; + $val->defaultScheme = 'http'; + $obj->attr_url = 'google.de'; + $val->validateAttribute($obj, 'attr_url'); + $this->assertFalse($obj->hasErrors('attr_url')); + $this->assertTrue(stripos($obj->attr_url, 'http') !== false); + $obj = new FakedValidationModel; + $obj->attr_url = 'gttp;/invalid string'; + $val->validateAttribute($obj, 'attr_url'); + $this->assertTrue($obj->hasErrors('attr_url')); + } } diff --git a/tests/unit/framework/validators/ValidatorTest.php b/tests/unit/framework/validators/ValidatorTest.php index e52ccdb7090..d1b28602985 100644 --- a/tests/unit/framework/validators/ValidatorTest.php +++ b/tests/unit/framework/validators/ValidatorTest.php @@ -2,7 +2,6 @@ namespace yiiunit\framework\validators; - use yii\validators\BooleanValidator; use yii\validators\InlineValidator; use yii\validators\NumberValidator; @@ -12,221 +11,222 @@ class ValidatorTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - protected function getTestModel($additionalAttributes = []) - { - $attributes = array_merge( - ['attr_runMe1' => true, 'attr_runMe2' => true, 'attr_skip' => true], - $additionalAttributes - ); - return FakedValidationModel::createWithAttributes($attributes); - } - - public function testCreateValidator() - { - $model = FakedValidationModel::createWithAttributes(['attr_test1' => 'abc', 'attr_test2' => '2013']); - /** @var NumberValidator $numberVal */ - $numberVal = TestValidator::createValidator('number', $model, ['attr_test1']); - $this->assertInstanceOf(NumberValidator::className(), $numberVal); - $numberVal = TestValidator::createValidator('integer', $model, ['attr_test2']); - $this->assertInstanceOf(NumberValidator::className(), $numberVal); - $this->assertTrue($numberVal->integerOnly); - $val = TestValidator::createValidator( - 'boolean', - $model, - ['attr_test1', 'attr_test2'], - ['on' => ['a', 'b']] - ); - $this->assertInstanceOf(BooleanValidator::className(), $val); - $this->assertSame(['a', 'b'], $val->on); - $this->assertSame(['attr_test1', 'attr_test2'], $val->attributes); - $val = TestValidator::createValidator( - 'boolean', - $model, - ['attr_test1', 'attr_test2'], - ['on' => ['a', 'b'], 'except' => ['c', 'd', 'e']] - ); - $this->assertInstanceOf(BooleanValidator::className(), $val); - $this->assertSame(['a', 'b'], $val->on); - $this->assertSame(['c', 'd', 'e'], $val->except); - $val = TestValidator::createValidator('inlineVal', $model, ['val_attr_a']); - $this->assertInstanceOf(InlineValidator::className(), $val); - $this->assertSame('inlineVal', $val->method); - } - - public function testValidate() - { - $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2']]); - $model = $this->getTestModel(); - $val->validateAttributes($model); - $this->assertTrue($val->isAttributeValidated('attr_runMe1')); - $this->assertTrue($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_skip')); - } - - public function testValidateWithAttributeIntersect() - { - $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2']]); - $model = $this->getTestModel(); - $val->validateAttributes($model, ['attr_runMe1']); - $this->assertTrue($val->isAttributeValidated('attr_runMe1')); - $this->assertFalse($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_skip')); - } - - public function testValidateWithEmptyAttributes() - { - $val = new TestValidator(); - $model = $this->getTestModel(); - $val->validateAttributes($model, ['attr_runMe1']); - $this->assertFalse($val->isAttributeValidated('attr_runMe1')); - $this->assertFalse($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_skip')); - $val->validateAttributes($model); - $this->assertFalse($val->isAttributeValidated('attr_runMe1')); - $this->assertFalse($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_skip')); - } - - public function testValidateWithError() - { - $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2'], 'skipOnError' => false]); - $model = $this->getTestModel(); - $val->validateAttributes($model); - $this->assertTrue($val->isAttributeValidated('attr_runMe1')); - $this->assertTrue($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_skip')); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe2')); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); - $val->validateAttributes($model, ['attr_runMe2']); - $this->assertEquals(2, $val->countAttributeValidations('attr_runMe2')); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); - $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); - $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2'], 'skipOnError' => true]); - $model = $this->getTestModel(); - $val->enableErrorOnValidateAttribute(); - $val->validateAttributes($model); - $this->assertTrue($val->isAttributeValidated('attr_runMe1')); - $this->assertTrue($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_skip')); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); - $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); - $val->validateAttributes($model, ['attr_runMe2']); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe2')); - $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); - $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); - } - - public function testValidateWithEmpty() - { - $val = new TestValidator([ - 'attributes' => [ - 'attr_runMe1', - 'attr_runMe2', - 'attr_empty1', - 'attr_empty2' - ], - 'skipOnEmpty' => true, - ]); - $model = $this->getTestModel(['attr_empty1' => '', 'attr_emtpy2' => ' ']); - $val->validateAttributes($model); - $this->assertTrue($val->isAttributeValidated('attr_runMe1')); - $this->assertTrue($val->isAttributeValidated('attr_runMe2')); - $this->assertFalse($val->isAttributeValidated('attr_empty1')); - $this->assertFalse($val->isAttributeValidated('attr_empty2')); - $model->attr_empty1 = 'not empty anymore'; - $val->validateAttributes($model); - $this->assertTrue($val->isAttributeValidated('attr_empty1')); - $this->assertFalse($val->isAttributeValidated('attr_empty2')); - $val = new TestValidator([ - 'attributes' => [ - 'attr_runMe1', - 'attr_runMe2', - 'attr_empty1', - 'attr_empty2' - ], - 'skipOnEmpty' => false, - ]); - $model = $this->getTestModel(['attr_empty1' => '', 'attr_emtpy2' => ' ']); - $val->validateAttributes($model); - $this->assertTrue($val->isAttributeValidated('attr_runMe1')); - $this->assertTrue($val->isAttributeValidated('attr_runMe2')); - $this->assertTrue($val->isAttributeValidated('attr_empty1')); - $this->assertTrue($val->isAttributeValidated('attr_empty2')); - } - - public function testIsEmpty() - { - $val = new TestValidator(); - $this->assertTrue($val->isEmpty(null)); - $this->assertTrue($val->isEmpty([])); - $this->assertTrue($val->isEmpty('')); - $this->assertFalse($val->isEmpty(5)); - $this->assertFalse($val->isEmpty(0)); - $this->assertFalse($val->isEmpty(new \stdClass())); - $this->assertFalse($val->isEmpty(' ')); - // trim - $this->assertTrue($val->isEmpty(' ', true)); - $this->assertTrue($val->isEmpty('', true)); - $this->assertTrue($val->isEmpty(" \t\n\r\0\x0B", true)); - $this->assertTrue($val->isEmpty('', true)); - $this->assertFalse($val->isEmpty('0', true)); - $this->assertFalse($val->isEmpty(0, true)); - $this->assertFalse($val->isEmpty('this ain\'t an empty value', true)); - } - - public function testValidateValue() - { - $this->setExpectedException( - 'yii\base\NotSupportedException', - TestValidator::className() . ' does not support validateValue().' - ); - $val = new TestValidator(); - $val->validate('abc'); - } - - public function testClientValidateAttribute() - { - $val = new TestValidator(); - $this->assertNull( - $val->clientValidateAttribute($this->getTestModel(), 'attr_runMe1', []) - ); //todo pass a view instead of array - } - - public function testIsActive() - { - $val = new TestValidator(); - $this->assertTrue($val->isActive('scenA')); - $this->assertTrue($val->isActive('scenB')); - $val->except = ['scenB']; - $this->assertTrue($val->isActive('scenA')); - $this->assertFalse($val->isActive('scenB')); - $val->on = ['scenC']; - $this->assertFalse($val->isActive('scenA')); - $this->assertFalse($val->isActive('scenB')); - $this->assertTrue($val->isActive('scenC')); - } - - public function testAddError() - { - $val = new TestValidator(); - $m = $this->getTestModel(['attr_msg_val' => 'abc']); - $val->addError($m, 'attr_msg_val', '{attribute}::{value}'); - $errors = $m->getErrors('attr_msg_val'); - $this->assertEquals('attr_msg_val::abc', $errors[0]); - $m = $this->getTestModel(['attr_msg_val' => ['bcc']]); - $val->addError($m, 'attr_msg_val', '{attribute}::{value}'); - $errors = $m->getErrors('attr_msg_val'); - $this->assertEquals('attr_msg_val::array()', $errors[0]); - $m = $this->getTestModel(['attr_msg_val' => 'abc']); - $val->addError($m, 'attr_msg_val', '{attribute}::{value}::{param}', ['param' => 'param_value']); - $errors = $m->getErrors('attr_msg_val'); - $this->assertEquals('attr_msg_val::abc::param_value', $errors[0]); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + + protected function getTestModel($additionalAttributes = []) + { + $attributes = array_merge( + ['attr_runMe1' => true, 'attr_runMe2' => true, 'attr_skip' => true], + $additionalAttributes + ); + + return FakedValidationModel::createWithAttributes($attributes); + } + + public function testCreateValidator() + { + $model = FakedValidationModel::createWithAttributes(['attr_test1' => 'abc', 'attr_test2' => '2013']); + /** @var NumberValidator $numberVal */ + $numberVal = TestValidator::createValidator('number', $model, ['attr_test1']); + $this->assertInstanceOf(NumberValidator::className(), $numberVal); + $numberVal = TestValidator::createValidator('integer', $model, ['attr_test2']); + $this->assertInstanceOf(NumberValidator::className(), $numberVal); + $this->assertTrue($numberVal->integerOnly); + $val = TestValidator::createValidator( + 'boolean', + $model, + ['attr_test1', 'attr_test2'], + ['on' => ['a', 'b']] + ); + $this->assertInstanceOf(BooleanValidator::className(), $val); + $this->assertSame(['a', 'b'], $val->on); + $this->assertSame(['attr_test1', 'attr_test2'], $val->attributes); + $val = TestValidator::createValidator( + 'boolean', + $model, + ['attr_test1', 'attr_test2'], + ['on' => ['a', 'b'], 'except' => ['c', 'd', 'e']] + ); + $this->assertInstanceOf(BooleanValidator::className(), $val); + $this->assertSame(['a', 'b'], $val->on); + $this->assertSame(['c', 'd', 'e'], $val->except); + $val = TestValidator::createValidator('inlineVal', $model, ['val_attr_a']); + $this->assertInstanceOf(InlineValidator::className(), $val); + $this->assertSame('inlineVal', $val->method); + } + + public function testValidate() + { + $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2']]); + $model = $this->getTestModel(); + $val->validateAttributes($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + } + + public function testValidateWithAttributeIntersect() + { + $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2']]); + $model = $this->getTestModel(); + $val->validateAttributes($model, ['attr_runMe1']); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + } + + public function testValidateWithEmptyAttributes() + { + $val = new TestValidator(); + $model = $this->getTestModel(); + $val->validateAttributes($model, ['attr_runMe1']); + $this->assertFalse($val->isAttributeValidated('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + $val->validateAttributes($model); + $this->assertFalse($val->isAttributeValidated('attr_runMe1')); + $this->assertFalse($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + } + + public function testValidateWithError() + { + $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2'], 'skipOnError' => false]); + $model = $this->getTestModel(); + $val->validateAttributes($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $val->validateAttributes($model, ['attr_runMe2']); + $this->assertEquals(2, $val->countAttributeValidations('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); + $val = new TestValidator(['attributes' => ['attr_runMe1', 'attr_runMe2'], 'skipOnError' => true]); + $model = $this->getTestModel(); + $val->enableErrorOnValidateAttribute(); + $val->validateAttributes($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_skip')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); + $val->validateAttributes($model, ['attr_runMe2']); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe2')); + $this->assertEquals(1, $val->countAttributeValidations('attr_runMe1')); + $this->assertEquals(0, $val->countAttributeValidations('attr_skip')); + } + + public function testValidateWithEmpty() + { + $val = new TestValidator([ + 'attributes' => [ + 'attr_runMe1', + 'attr_runMe2', + 'attr_empty1', + 'attr_empty2' + ], + 'skipOnEmpty' => true, + ]); + $model = $this->getTestModel(['attr_empty1' => '', 'attr_emtpy2' => ' ']); + $val->validateAttributes($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertFalse($val->isAttributeValidated('attr_empty1')); + $this->assertFalse($val->isAttributeValidated('attr_empty2')); + $model->attr_empty1 = 'not empty anymore'; + $val->validateAttributes($model); + $this->assertTrue($val->isAttributeValidated('attr_empty1')); + $this->assertFalse($val->isAttributeValidated('attr_empty2')); + $val = new TestValidator([ + 'attributes' => [ + 'attr_runMe1', + 'attr_runMe2', + 'attr_empty1', + 'attr_empty2' + ], + 'skipOnEmpty' => false, + ]); + $model = $this->getTestModel(['attr_empty1' => '', 'attr_emtpy2' => ' ']); + $val->validateAttributes($model); + $this->assertTrue($val->isAttributeValidated('attr_runMe1')); + $this->assertTrue($val->isAttributeValidated('attr_runMe2')); + $this->assertTrue($val->isAttributeValidated('attr_empty1')); + $this->assertTrue($val->isAttributeValidated('attr_empty2')); + } + + public function testIsEmpty() + { + $val = new TestValidator(); + $this->assertTrue($val->isEmpty(null)); + $this->assertTrue($val->isEmpty([])); + $this->assertTrue($val->isEmpty('')); + $this->assertFalse($val->isEmpty(5)); + $this->assertFalse($val->isEmpty(0)); + $this->assertFalse($val->isEmpty(new \stdClass())); + $this->assertFalse($val->isEmpty(' ')); + // trim + $this->assertTrue($val->isEmpty(' ', true)); + $this->assertTrue($val->isEmpty('', true)); + $this->assertTrue($val->isEmpty(" \t\n\r\0\x0B", true)); + $this->assertTrue($val->isEmpty('', true)); + $this->assertFalse($val->isEmpty('0', true)); + $this->assertFalse($val->isEmpty(0, true)); + $this->assertFalse($val->isEmpty('this ain\'t an empty value', true)); + } + + public function testValidateValue() + { + $this->setExpectedException( + 'yii\base\NotSupportedException', + TestValidator::className() . ' does not support validateValue().' + ); + $val = new TestValidator(); + $val->validate('abc'); + } + + public function testClientValidateAttribute() + { + $val = new TestValidator(); + $this->assertNull( + $val->clientValidateAttribute($this->getTestModel(), 'attr_runMe1', []) + ); //todo pass a view instead of array + } + + public function testIsActive() + { + $val = new TestValidator(); + $this->assertTrue($val->isActive('scenA')); + $this->assertTrue($val->isActive('scenB')); + $val->except = ['scenB']; + $this->assertTrue($val->isActive('scenA')); + $this->assertFalse($val->isActive('scenB')); + $val->on = ['scenC']; + $this->assertFalse($val->isActive('scenA')); + $this->assertFalse($val->isActive('scenB')); + $this->assertTrue($val->isActive('scenC')); + } + + public function testAddError() + { + $val = new TestValidator(); + $m = $this->getTestModel(['attr_msg_val' => 'abc']); + $val->addError($m, 'attr_msg_val', '{attribute}::{value}'); + $errors = $m->getErrors('attr_msg_val'); + $this->assertEquals('attr_msg_val::abc', $errors[0]); + $m = $this->getTestModel(['attr_msg_val' => ['bcc']]); + $val->addError($m, 'attr_msg_val', '{attribute}::{value}'); + $errors = $m->getErrors('attr_msg_val'); + $this->assertEquals('attr_msg_val::array()', $errors[0]); + $m = $this->getTestModel(['attr_msg_val' => 'abc']); + $val->addError($m, 'attr_msg_val', '{attribute}::{value}::{param}', ['param' => 'param_value']); + $errors = $m->getErrors('attr_msg_val'); + $this->assertEquals('attr_msg_val::abc::param_value', $errors[0]); + } } diff --git a/tests/unit/framework/web/AssetBundleTest.php b/tests/unit/framework/web/AssetBundleTest.php index 05be6030ffa..c44635cf6ae 100644 --- a/tests/unit/framework/web/AssetBundleTest.php +++ b/tests/unit/framework/web/AssetBundleTest.php @@ -17,240 +17,239 @@ */ class AssetBundleTest extends \yiiunit\TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - - Yii::setAlias('@testWeb', '/'); - Yii::setAlias('@testWebRoot', '@yiiunit/data/web'); - } - - protected function getView() - { - $view = new View(); - $view->setAssetManager(new AssetManager([ - 'basePath' => '@testWebRoot/assets', - 'baseUrl' => '@testWeb/assets', - ])); - - return $view; - } - - public function testRegister() - { - $view = $this->getView(); - - $this->assertEmpty($view->assetBundles); - TestSimpleAsset::register($view); - $this->assertEquals(1, count($view->assetBundles)); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestSimpleAsset', $view->assetBundles); - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestSimpleAsset'] instanceof AssetBundle); - - $expected = <<mockApplication(); + + Yii::setAlias('@testWeb', '/'); + Yii::setAlias('@testWebRoot', '@yiiunit/data/web'); + } + + protected function getView() + { + $view = new View(); + $view->setAssetManager(new AssetManager([ + 'basePath' => '@testWebRoot/assets', + 'baseUrl' => '@testWeb/assets', + ])); + + return $view; + } + + public function testRegister() + { + $view = $this->getView(); + + $this->assertEmpty($view->assetBundles); + TestSimpleAsset::register($view); + $this->assertEquals(1, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestSimpleAsset', $view->assetBundles); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestSimpleAsset'] instanceof AssetBundle); + + $expected = <<4 EOF; - $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); - } - - public function testSimpleDependency() - { - $view = $this->getView(); - - $this->assertEmpty($view->assetBundles); - TestAssetBundle::register($view); - $this->assertEquals(3, count($view->assetBundles)); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); - - $expected = <<assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function testSimpleDependency() + { + $view = $this->getView(); + + $this->assertEmpty($view->assetBundles); + TestAssetBundle::register($view); + $this->assertEquals(3, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); + + $expected = <<23 4 EOF; - $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); - } - - public function positionProvider() - { - return [ - [View::POS_HEAD, true], - [View::POS_HEAD, false], - [View::POS_BEGIN, true], - [View::POS_BEGIN, false], - [View::POS_END, true], - [View::POS_END, false], - ]; - } - - /** - * @dataProvider positionProvider - */ - public function testPositionDependency($pos, $jqAlreadyRegistered) - { - $view = $this->getView(); - - $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = [ - 'jsOptions' => [ - 'position' => $pos, - ], - ]; - - $this->assertEmpty($view->assetBundles); - if ($jqAlreadyRegistered) { - TestJqueryAsset::register($view); - } - TestAssetBundle::register($view); - $this->assertEquals(3, count($view->assetBundles)); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); - $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); - - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); - $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); - - $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions); - $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions['position']); - $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions); - $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions['position']); - $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions); - $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions['position']); - - switch($pos) - { - case View::POS_HEAD: - $expected = <<assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function positionProvider() + { + return [ + [View::POS_HEAD, true], + [View::POS_HEAD, false], + [View::POS_BEGIN, true], + [View::POS_BEGIN, false], + [View::POS_END, true], + [View::POS_END, false], + ]; + } + + /** + * @dataProvider positionProvider + */ + public function testPositionDependency($pos, $jqAlreadyRegistered) + { + $view = $this->getView(); + + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = [ + 'jsOptions' => [ + 'position' => $pos, + ], + ]; + + $this->assertEmpty($view->assetBundles); + if ($jqAlreadyRegistered) { + TestJqueryAsset::register($view); + } + TestAssetBundle::register($view); + $this->assertEquals(3, count($view->assetBundles)); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetBundle', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestJqueryAsset', $view->assetBundles); + $this->assertArrayHasKey('yiiunit\\framework\\web\\TestAssetLevel3', $view->assetBundles); + + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset'] instanceof AssetBundle); + $this->assertTrue($view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3'] instanceof AssetBundle); + + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetBundle']->jsOptions['position']); + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestJqueryAsset']->jsOptions['position']); + $this->assertArrayHasKey('position', $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions); + $this->assertEquals($pos, $view->assetBundles['yiiunit\\framework\\web\\TestAssetLevel3']->jsOptions['position']); + + switch ($pos) { + case View::POS_HEAD: + $expected = << 234 EOF; - break; - case View::POS_BEGIN: - $expected = <<2 34 EOF; - break; - default: - case View::POS_END: - $expected = <<23 4 EOF; - break; - } - $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); - } - - public function positionProvider2() - { - return [ - [View::POS_BEGIN, true], - [View::POS_BEGIN, false], - [View::POS_END, true], - [View::POS_END, false], - ]; - } - - /** - * @dataProvider positionProvider - */ - public function testPositionDependencyConflict($pos, $jqAlreadyRegistered) - { - $view = $this->getView(); - - $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = [ - 'jsOptions' => [ - 'position' => $pos - 1, - ], - ]; - $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestJqueryAsset'] = [ - 'jsOptions' => [ - 'position' => $pos, - ], - ]; - - $this->assertEmpty($view->assetBundles); - if ($jqAlreadyRegistered) { - TestJqueryAsset::register($view); - } - $this->setExpectedException('yii\\base\\InvalidConfigException'); - TestAssetBundle::register($view); - } - - public function testCircularDependency() - { - $this->setExpectedException('yii\\base\\InvalidConfigException'); - TestAssetCircleA::register($this->getView()); - } + break; + } + $this->assertEquals($expected, $view->renderFile('@yiiunit/data/views/rawlayout.php')); + } + + public function positionProvider2() + { + return [ + [View::POS_BEGIN, true], + [View::POS_BEGIN, false], + [View::POS_END, true], + [View::POS_END, false], + ]; + } + + /** + * @dataProvider positionProvider + */ + public function testPositionDependencyConflict($pos, $jqAlreadyRegistered) + { + $view = $this->getView(); + + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestAssetBundle'] = [ + 'jsOptions' => [ + 'position' => $pos - 1, + ], + ]; + $view->getAssetManager()->bundles['yiiunit\\framework\\web\\TestJqueryAsset'] = [ + 'jsOptions' => [ + 'position' => $pos, + ], + ]; + + $this->assertEmpty($view->assetBundles); + if ($jqAlreadyRegistered) { + TestJqueryAsset::register($view); + } + $this->setExpectedException('yii\\base\\InvalidConfigException'); + TestAssetBundle::register($view); + } + + public function testCircularDependency() + { + $this->setExpectedException('yii\\base\\InvalidConfigException'); + TestAssetCircleA::register($this->getView()); + } } class TestSimpleAsset extends AssetBundle { - public $basePath = '@testWebRoot/js'; - public $baseUrl = '@testWeb/js'; - public $js = [ - 'jquery.js', - ]; + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = [ + 'jquery.js', + ]; } class TestAssetBundle extends AssetBundle { - public $basePath = '@testWebRoot/files'; - public $baseUrl = '@testWeb/files'; - public $css = [ - 'cssFile.css', - ]; - public $js = [ - 'jsFile.js', - ]; - public $depends = [ - 'yiiunit\\framework\\web\\TestJqueryAsset' - ]; + public $basePath = '@testWebRoot/files'; + public $baseUrl = '@testWeb/files'; + public $css = [ + 'cssFile.css', + ]; + public $js = [ + 'jsFile.js', + ]; + public $depends = [ + 'yiiunit\\framework\\web\\TestJqueryAsset' + ]; } class TestJqueryAsset extends AssetBundle { - public $basePath = '@testWebRoot/js'; - public $baseUrl = '@testWeb/js'; - public $js = [ - 'jquery.js', - ]; - public $depends = [ - 'yiiunit\\framework\\web\\TestAssetLevel3' - ]; + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = [ + 'jquery.js', + ]; + public $depends = [ + 'yiiunit\\framework\\web\\TestAssetLevel3' + ]; } class TestAssetLevel3 extends AssetBundle { - public $basePath = '@testWebRoot/js'; - public $baseUrl = '@testWeb/js'; + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; } class TestAssetCircleA extends AssetBundle { - public $basePath = '@testWebRoot/js'; - public $baseUrl = '@testWeb/js'; - public $js = [ - 'jquery.js', - ]; - public $depends = [ - 'yiiunit\\framework\\web\\TestAssetCircleB' - ]; + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = [ + 'jquery.js', + ]; + public $depends = [ + 'yiiunit\\framework\\web\\TestAssetCircleB' + ]; } class TestAssetCircleB extends AssetBundle { - public $basePath = '@testWebRoot/js'; - public $baseUrl = '@testWeb/js'; - public $js = [ - 'jquery.js', - ]; - public $depends = [ - 'yiiunit\\framework\\web\\TestAssetCircleA' - ]; + public $basePath = '@testWebRoot/js'; + public $baseUrl = '@testWeb/js'; + public $js = [ + 'jquery.js', + ]; + public $depends = [ + 'yiiunit\\framework\\web\\TestAssetCircleA' + ]; } diff --git a/tests/unit/framework/web/AssetConverterTest.php b/tests/unit/framework/web/AssetConverterTest.php index 1c857b8cf46..11822f37379 100644 --- a/tests/unit/framework/web/AssetConverterTest.php +++ b/tests/unit/framework/web/AssetConverterTest.php @@ -12,32 +12,31 @@ */ class AssetConverterTest extends \yiiunit\TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } - - - public function testConvert() - { - $tmpPath = \Yii::$app->runtimePath . '/assetConverterTest'; - if (!is_dir($tmpPath)) { - mkdir($tmpPath, 0777, true); - } - file_put_contents($tmpPath . '/test.php', <<mockApplication(); + } + + public function testConvert() + { + $tmpPath = \Yii::$app->runtimePath . '/assetConverterTest'; + if (!is_dir($tmpPath)) { + mkdir($tmpPath, 0777, true); + } + file_put_contents($tmpPath . '/test.php', <<commands['php'] = ['txt', 'php {from} > {to}']; - $this->assertEquals('test.txt', $converter->convert('test.php', $tmpPath)); + $converter = new AssetConverter(); + $converter->commands['php'] = ['txt', 'php {from} > {to}']; + $this->assertEquals('test.txt', $converter->convert('test.php', $tmpPath)); - $this->assertTrue(file_exists($tmpPath . '/test.txt'), 'Failed asserting that asset output file exists.'); - $this->assertEquals("Hello World!\nHello Yii!", file_get_contents($tmpPath . '/test.txt')); - } + $this->assertTrue(file_exists($tmpPath . '/test.txt'), 'Failed asserting that asset output file exists.'); + $this->assertEquals("Hello World!\nHello Yii!", file_get_contents($tmpPath . '/test.txt')); + } } diff --git a/tests/unit/framework/web/CacheSessionTest.php b/tests/unit/framework/web/CacheSessionTest.php index c593691ca07..fa029bfd631 100644 --- a/tests/unit/framework/web/CacheSessionTest.php +++ b/tests/unit/framework/web/CacheSessionTest.php @@ -11,26 +11,26 @@ */ class CacheSessionTest extends \yiiunit\TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - Yii::$app->setComponent('cache', new FileCache()); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + Yii::$app->setComponent('cache', new FileCache()); + } - public function testCacheSession() - { - $session = new CacheSession(); + public function testCacheSession() + { + $session = new CacheSession(); - $session->writeSession('test', 'sessionData'); - $this->assertEquals('sessionData', $session->readSession('test')); - $session->destroySession('test'); - $this->assertEquals('', $session->readSession('test')); - } + $session->writeSession('test', 'sessionData'); + $this->assertEquals('sessionData', $session->readSession('test')); + $session->destroySession('test'); + $this->assertEquals('', $session->readSession('test')); + } - public function testInvalidCache() - { - $this->setExpectedException('yii\base\InvalidConfigException'); - new CacheSession(['cache' => 'invalid']); - } + public function testInvalidCache() + { + $this->setExpectedException('yii\base\InvalidConfigException'); + new CacheSession(['cache' => 'invalid']); + } } diff --git a/tests/unit/framework/web/RequestTest.php b/tests/unit/framework/web/RequestTest.php index 656a5170d68..5de536c8b44 100644 --- a/tests/unit/framework/web/RequestTest.php +++ b/tests/unit/framework/web/RequestTest.php @@ -16,26 +16,26 @@ */ class RequestTest extends TestCase { - public function testParseAcceptHeader() - { - $request = new Request; + public function testParseAcceptHeader() + { + $request = new Request; - $this->assertEquals([], $request->parseAcceptHeader(' ')); + $this->assertEquals([], $request->parseAcceptHeader(' ')); - $this->assertEquals([ - 'audio/basic' => ['q' => 1], - 'audio/*' => ['q' => 0.2], - ], $request->parseAcceptHeader('audio/*; q=0.2, audio/basic')); + $this->assertEquals([ + 'audio/basic' => ['q' => 1], + 'audio/*' => ['q' => 0.2], + ], $request->parseAcceptHeader('audio/*; q=0.2, audio/basic')); - $this->assertEquals([ - 'application/json' => ['q' => 1, 'version' => '1.0'], - 'application/xml' => ['q' => 1, 'version' => '2.0', 'x'], - 'text/x-c' => ['q' => 1], - 'text/x-dvi' => ['q' => 0.8], - 'text/plain' => ['q' => 0.5], - ], $request->parseAcceptHeader('text/plain; q=0.5, - application/json; version=1.0, - application/xml; version=2.0; x, - text/x-dvi; q=0.8, text/x-c')); - } + $this->assertEquals([ + 'application/json' => ['q' => 1, 'version' => '1.0'], + 'application/xml' => ['q' => 1, 'version' => '2.0', 'x'], + 'text/x-c' => ['q' => 1], + 'text/x-dvi' => ['q' => 0.8], + 'text/plain' => ['q' => 0.5], + ], $request->parseAcceptHeader('text/plain; q=0.5, + application/json; version=1.0, + application/xml; version=2.0; x, + text/x-dvi; q=0.8, text/x-c')); + } } diff --git a/tests/unit/framework/web/ResponseTest.php b/tests/unit/framework/web/ResponseTest.php index 89c2a40fd7c..6d942478e85 100644 --- a/tests/unit/framework/web/ResponseTest.php +++ b/tests/unit/framework/web/ResponseTest.php @@ -10,71 +10,71 @@ */ class ResponseTest extends \yiiunit\TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - $this->response = new \yii\web\Response; - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->response = new \yii\web\Response; + } - public function rightRanges() - { - // TODO test more cases for range requests and check for rfc compatibility - // http://www.w3.org/Protocols/rfc2616/rfc2616.txt - return [ - ['0-5', '0-5', 6, '12ёж'], - ['2-', '2-66', 65, 'ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'], - ['-12', '55-66', 12, '(ёжик)=?'], - ]; - } + public function rightRanges() + { + // TODO test more cases for range requests and check for rfc compatibility + // http://www.w3.org/Protocols/rfc2616/rfc2616.txt + return [ + ['0-5', '0-5', 6, '12ёж'], + ['2-', '2-66', 65, 'ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'], + ['-12', '55-66', 12, '(ёжик)=?'], + ]; + } - /** - * @dataProvider rightRanges - */ - public function testSendFileRanges($rangeHeader, $expectedHeader, $length, $expectedContent) - { - $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); - $fullContent = file_get_contents($dataFile); - $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; - ob_start(); - $this->response->sendFile($dataFile)->send( ); - $content = ob_get_clean(); + /** + * @dataProvider rightRanges + */ + public function testSendFileRanges($rangeHeader, $expectedHeader, $length, $expectedContent) + { + $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); + $fullContent = file_get_contents($dataFile); + $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; + ob_start(); + $this->response->sendFile($dataFile)->send( ); + $content = ob_get_clean(); - $this->assertEquals($expectedContent, $content); - $this->assertEquals(206, $this->response->statusCode); - $headers = $this->response->headers; - $this->assertEquals("bytes", $headers->get('Accept-Ranges')); - $this->assertEquals("bytes " . $expectedHeader . '/' . StringHelper::byteLength($fullContent), $headers->get('Content-Range')); - $this->assertEquals('text/plain', $headers->get('Content-Type')); - $this->assertEquals("$length", $headers->get('Content-Length')); - } + $this->assertEquals($expectedContent, $content); + $this->assertEquals(206, $this->response->statusCode); + $headers = $this->response->headers; + $this->assertEquals("bytes", $headers->get('Accept-Ranges')); + $this->assertEquals("bytes " . $expectedHeader . '/' . StringHelper::byteLength($fullContent), $headers->get('Content-Range')); + $this->assertEquals('text/plain', $headers->get('Content-Type')); + $this->assertEquals("$length", $headers->get('Content-Length')); + } - public function wrongRanges() - { - // TODO test more cases for range requests and check for rfc compatibility - // http://www.w3.org/Protocols/rfc2616/rfc2616.txt - return [ - ['1-2,3-5,6-10'], // multiple range request not supported - ['5-1'], // last-byte-pos value is less than its first-byte-pos value - ['-100000'], // last-byte-pos bigger then content length - ['10000-'], // first-byte-pos bigger then content length - ]; - } + public function wrongRanges() + { + // TODO test more cases for range requests and check for rfc compatibility + // http://www.w3.org/Protocols/rfc2616/rfc2616.txt + return [ + ['1-2,3-5,6-10'], // multiple range request not supported + ['5-1'], // last-byte-pos value is less than its first-byte-pos value + ['-100000'], // last-byte-pos bigger then content length + ['10000-'], // first-byte-pos bigger then content length + ]; + } - /** - * @dataProvider wrongRanges - */ - public function testSendFileWrongRanges($rangeHeader) - { - $this->setExpectedException('yii\web\HttpException'); + /** + * @dataProvider wrongRanges + */ + public function testSendFileWrongRanges($rangeHeader) + { + $this->setExpectedException('yii\web\HttpException'); - $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); - $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; - $this->response->sendFile($dataFile); - } + $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt'); + $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; + $this->response->sendFile($dataFile); + } - protected function generateTestFileContent() - { - return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; - } + protected function generateTestFileContent() + { + return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; + } } diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php index 79e4905795e..fae751a2a05 100644 --- a/tests/unit/framework/web/UrlManagerTest.php +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -10,308 +10,308 @@ */ class UrlManagerTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testCreateUrl() - { - // default setting with '/' as base url - $manager = new UrlManager([ - 'baseUrl' => '/', - 'cache' => null, - ]); - $url = $manager->createUrl(['post/view']); - $this->assertEquals('?r=post/view', $url); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('?r=post/view&id=1&title=sample+post', $url); + public function testCreateUrl() + { + // default setting with '/' as base url + $manager = new UrlManager([ + 'baseUrl' => '/', + 'cache' => null, + ]); + $url = $manager->createUrl(['post/view']); + $this->assertEquals('?r=post/view', $url); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('?r=post/view&id=1&title=sample+post', $url); - // default setting with '/test/' as base url - $manager = new UrlManager([ - 'baseUrl' => '/test/', - 'cache' => null, - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('/test?r=post/view&id=1&title=sample+post', $url); + // default setting with '/test/' as base url + $manager = new UrlManager([ + 'baseUrl' => '/test/', + 'cache' => null, + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('/test?r=post/view&id=1&title=sample+post', $url); - // pretty URL without rules - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'baseUrl' => '/', - 'cache' => null, - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('/post/view?id=1&title=sample+post', $url); - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'baseUrl' => '/test/', - 'cache' => null, - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('/test/post/view?id=1&title=sample+post', $url); - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'baseUrl' => '/test/index.php', - 'cache' => null, - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('/test/index.php/post/view?id=1&title=sample+post', $url); + // pretty URL without rules + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'baseUrl' => '/', + 'cache' => null, + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('/post/view?id=1&title=sample+post', $url); + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'baseUrl' => '/test/', + 'cache' => null, + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('/test/post/view?id=1&title=sample+post', $url); + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'baseUrl' => '/test/index.php', + 'cache' => null, + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('/test/index.php/post/view?id=1&title=sample+post', $url); - // todo: test showScriptName + // todo: test showScriptName - // pretty URL with rules - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'cache' => null, - 'rules' => [ - [ - 'pattern' => 'post//', - 'route' => 'post/view', - ], - ], - 'baseUrl' => '/', - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('/post/1/sample+post', $url); - $url = $manager->createUrl(['post/index', 'page' => 1]); - $this->assertEquals('/post/index?page=1', $url); + // pretty URL with rules + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => [ + [ + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ], + ], + 'baseUrl' => '/', + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('/post/1/sample+post', $url); + $url = $manager->createUrl(['post/index', 'page' => 1]); + $this->assertEquals('/post/index?page=1', $url); - // pretty URL with rules and suffix - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'cache' => null, - 'rules' => [ - [ - 'pattern' => 'post/<id>/<title>', - 'route' => 'post/view', - ], - ], - 'baseUrl' => '/', - 'suffix' => '.html', - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('/post/1/sample+post.html', $url); - $url = $manager->createUrl(['post/index', 'page' => 1]); - $this->assertEquals('/post/index.html?page=1', $url); + // pretty URL with rules and suffix + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => [ + [ + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ], + ], + 'baseUrl' => '/', + 'suffix' => '.html', + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('/post/1/sample+post.html', $url); + $url = $manager->createUrl(['post/index', 'page' => 1]); + $this->assertEquals('/post/index.html?page=1', $url); - // pretty URL with rules that have host info - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'cache' => null, - 'rules' => [ - [ - 'pattern' => 'post/<id>/<title>', - 'route' => 'post/view', - 'host' => 'http://<lang:en|fr>.example.com', - ], - ], - 'baseUrl' => '/test', - ]); - $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post', 'lang' => 'en']); - $this->assertEquals('http://en.example.com/test/post/1/sample+post', $url); - $url = $manager->createUrl(['post/index', 'page' => 1]); - $this->assertEquals('/test/post/index?page=1', $url); - } + // pretty URL with rules that have host info + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => [ + [ + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + 'host' => 'http://<lang:en|fr>.example.com', + ], + ], + 'baseUrl' => '/test', + ]); + $url = $manager->createUrl(['post/view', 'id' => 1, 'title' => 'sample post', 'lang' => 'en']); + $this->assertEquals('http://en.example.com/test/post/1/sample+post', $url); + $url = $manager->createUrl(['post/index', 'page' => 1]); + $this->assertEquals('/test/post/index?page=1', $url); + } - public function testCreateAbsoluteUrl() - { - $manager = new UrlManager([ - 'baseUrl' => '/', - 'hostInfo' => 'http://www.example.com', - 'cache' => null, - ]); - $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post']); - $this->assertEquals('http://www.example.com?r=post/view&id=1&title=sample+post', $url); + public function testCreateAbsoluteUrl() + { + $manager = new UrlManager([ + 'baseUrl' => '/', + 'hostInfo' => 'http://www.example.com', + 'cache' => null, + ]); + $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post']); + $this->assertEquals('http://www.example.com?r=post/view&id=1&title=sample+post', $url); - $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], 'https'); - $this->assertEquals('https://www.example.com?r=post/view&id=1&title=sample+post', $url); + $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], 'https'); + $this->assertEquals('https://www.example.com?r=post/view&id=1&title=sample+post', $url); - $manager->hostInfo = 'https://www.example.com'; - $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], 'http'); - $this->assertEquals('http://www.example.com?r=post/view&id=1&title=sample+post', $url); - } + $manager->hostInfo = 'https://www.example.com'; + $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], 'http'); + $this->assertEquals('http://www.example.com?r=post/view&id=1&title=sample+post', $url); + } - public function testParseRequest() - { - $manager = new UrlManager(['cache' => null]); - $request = new Request; + public function testParseRequest() + { + $manager = new UrlManager(['cache' => null]); + $request = new Request; - // default setting without 'r' param - unset($_GET['r']); - $result = $manager->parseRequest($request); - $this->assertEquals(['', []], $result); + // default setting without 'r' param + unset($_GET['r']); + $result = $manager->parseRequest($request); + $this->assertEquals(['', []], $result); - // default setting with 'r' param - $_GET['r'] = 'site/index'; - $result = $manager->parseRequest($request); - $this->assertEquals(['site/index', []], $result); + // default setting with 'r' param + $_GET['r'] = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(['site/index', []], $result); - // default setting with 'r' param as an array - $_GET['r'] = ['site/index']; - $result = $manager->parseRequest($request); - $this->assertEquals(['', []], $result); + // default setting with 'r' param as an array + $_GET['r'] = ['site/index']; + $result = $manager->parseRequest($request); + $this->assertEquals(['', []], $result); - // pretty URL without rules - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'cache' => null, - ]); - // empty pathinfo - $request->pathInfo = ''; - $result = $manager->parseRequest($request); - $this->assertEquals(['', []], $result); - // normal pathinfo - $request->pathInfo = 'site/index'; - $result = $manager->parseRequest($request); - $this->assertEquals(['site/index', []], $result); - // pathinfo with module - $request->pathInfo = 'module/site/index'; - $result = $manager->parseRequest($request); - $this->assertEquals(['module/site/index', []], $result); - // pathinfo with trailing slashes - $request->pathInfo = '/module/site/index/'; - $result = $manager->parseRequest($request); - $this->assertEquals(['module/site/index/', []], $result); + // pretty URL without rules + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'cache' => null, + ]); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(['', []], $result); + // normal pathinfo + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(['site/index', []], $result); + // pathinfo with module + $request->pathInfo = 'module/site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(['module/site/index', []], $result); + // pathinfo with trailing slashes + $request->pathInfo = '/module/site/index/'; + $result = $manager->parseRequest($request); + $this->assertEquals(['module/site/index/', []], $result); - // pretty URL rules - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'cache' => null, - 'rules' => [ - [ - 'pattern' => 'post/<id>/<title>', - 'route' => 'post/view', - ], - ], - ]); - // matching pathinfo - $request->pathInfo = 'post/123/this+is+sample'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); - // trailing slash is significant - $request->pathInfo = 'post/123/this+is+sample/'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/123/this+is+sample/', []], $result); - // empty pathinfo - $request->pathInfo = ''; - $result = $manager->parseRequest($request); - $this->assertEquals(['', []], $result); - // normal pathinfo - $request->pathInfo = 'site/index'; - $result = $manager->parseRequest($request); - $this->assertEquals(['site/index', []], $result); - // pathinfo with module - $request->pathInfo = 'module/site/index'; - $result = $manager->parseRequest($request); - $this->assertEquals(['module/site/index', []], $result); + // pretty URL rules + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => [ + [ + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ], + ], + ]); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); + // trailing slash is significant + $request->pathInfo = 'post/123/this+is+sample/'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/123/this+is+sample/', []], $result); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(['', []], $result); + // normal pathinfo + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(['site/index', []], $result); + // pathinfo with module + $request->pathInfo = 'module/site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(['module/site/index', []], $result); - // pretty URL rules - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'suffix' => '.html', - 'cache' => null, - 'rules' => [ - [ - 'pattern' => 'post/<id>/<title>', - 'route' => 'post/view', - ], - ], - ]); - // matching pathinfo - $request->pathInfo = 'post/123/this+is+sample.html'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); - // matching pathinfo without suffix - $request->pathInfo = 'post/123/this+is+sample'; - $result = $manager->parseRequest($request); - $this->assertFalse($result); - // empty pathinfo - $request->pathInfo = ''; - $result = $manager->parseRequest($request); - $this->assertEquals(['', []], $result); - // normal pathinfo - $request->pathInfo = 'site/index.html'; - $result = $manager->parseRequest($request); - $this->assertEquals(['site/index', []], $result); - // pathinfo without suffix - $request->pathInfo = 'site/index'; - $result = $manager->parseRequest($request); - $this->assertFalse($result); + // pretty URL rules + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'suffix' => '.html', + 'cache' => null, + 'rules' => [ + [ + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ], + ], + ]); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); + // matching pathinfo without suffix + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(['', []], $result); + // normal pathinfo + $request->pathInfo = 'site/index.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(['site/index', []], $result); + // pathinfo without suffix + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); - // strict parsing - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'enableStrictParsing' => true, - 'suffix' => '.html', - 'cache' => null, - 'rules' => [ - [ - 'pattern' => 'post/<id>/<title>', - 'route' => 'post/view', - ], - ], - ]); - // matching pathinfo - $request->pathInfo = 'post/123/this+is+sample.html'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); - // unmatching pathinfo - $request->pathInfo = 'site/index.html'; - $result = $manager->parseRequest($request); - $this->assertFalse($result); - } + // strict parsing + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'enableStrictParsing' => true, + 'suffix' => '.html', + 'cache' => null, + 'rules' => [ + [ + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ], + ], + ]); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); + // unmatching pathinfo + $request->pathInfo = 'site/index.html'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + } - public function testParseRESTRequest() - { - $request = new Request; + public function testParseRESTRequest() + { + $request = new Request; - // pretty URL rules - $manager = new UrlManager([ - 'enablePrettyUrl' => true, - 'showScriptName' => false, - 'cache' => null, - 'rules' => [ - 'PUT,POST post/<id>/<title>' => 'post/create', - 'DELETE post/<id>' => 'post/delete', - 'post/<id>/<title>' => 'post/view', - 'POST/GET' => 'post/get', - ], - ]); - // matching pathinfo GET request - $_SERVER['REQUEST_METHOD'] = 'GET'; - $request->pathInfo = 'post/123/this+is+sample'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); - // matching pathinfo PUT/POST request - $_SERVER['REQUEST_METHOD'] = 'PUT'; - $request->pathInfo = 'post/123/this+is+sample'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $request->pathInfo = 'post/123/this+is+sample'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result); + // pretty URL rules + $manager = new UrlManager([ + 'enablePrettyUrl' => true, + 'showScriptName' => false, + 'cache' => null, + 'rules' => [ + 'PUT,POST post/<id>/<title>' => 'post/create', + 'DELETE post/<id>' => 'post/delete', + 'post/<id>/<title>' => 'post/view', + 'POST/GET' => 'post/get', + ], + ]); + // matching pathinfo GET request + $_SERVER['REQUEST_METHOD'] = 'GET'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); + // matching pathinfo PUT/POST request + $_SERVER['REQUEST_METHOD'] = 'PUT'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result); + $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result); - // no wrong matching - $_SERVER['REQUEST_METHOD'] = 'POST'; - $request->pathInfo = 'POST/GET'; - $result = $manager->parseRequest($request); - $this->assertEquals(['post/get', []], $result); + // no wrong matching + $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->pathInfo = 'POST/GET'; + $result = $manager->parseRequest($request); + $this->assertEquals(['post/get', []], $result); - // createUrl should ignore REST rules - $this->mockApplication([ - 'components' => [ - 'request' => [ - 'hostInfo' => 'http://localhost/', - 'baseUrl' => '/app' - ] - ] - ], \yii\web\Application::className()); - $this->assertEquals('/app/post/delete?id=123', $manager->createUrl(['post/delete', 'id' => 123])); - $this->destroyApplication(); + // createUrl should ignore REST rules + $this->mockApplication([ + 'components' => [ + 'request' => [ + 'hostInfo' => 'http://localhost/', + 'baseUrl' => '/app' + ] + ] + ], \yii\web\Application::className()); + $this->assertEquals('/app/post/delete?id=123', $manager->createUrl(['post/delete', 'id' => 123])); + $this->destroyApplication(); - unset($_SERVER['REQUEST_METHOD']); - } + unset($_SERVER['REQUEST_METHOD']); + } } diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index b5bd1b5cf8b..3a3b39f42f9 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -12,677 +12,677 @@ */ class UrlRuleTest extends TestCase { - protected function setUp() - { - parent::setUp(); - $this->mockApplication(); - } + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } - public function testCreateUrl() - { - $manager = new UrlManager(['cache' => null]); - $suites = $this->getTestsForCreateUrl(); - foreach ($suites as $i => $suite) { - list ($name, $config, $tests) = $suite; - $rule = new UrlRule($config); - foreach ($tests as $j => $test) { - list ($route, $params, $expected) = $test; - $url = $rule->createUrl($manager, $route, $params); - $this->assertEquals($expected, $url, "Test#$i-$j: $name"); - } - } - } + public function testCreateUrl() + { + $manager = new UrlManager(['cache' => null]); + $suites = $this->getTestsForCreateUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + list ($route, $params, $expected) = $test; + $url = $rule->createUrl($manager, $route, $params); + $this->assertEquals($expected, $url, "Test#$i-$j: $name"); + } + } + } - public function testParseRequest() - { - $manager = new UrlManager(['cache' => null]); - $request = new Request(['hostInfo' => 'http://en.example.com']); - $suites = $this->getTestsForParseRequest(); - foreach ($suites as $i => $suite) { - list ($name, $config, $tests) = $suite; - $rule = new UrlRule($config); - foreach ($tests as $j => $test) { - $request->pathInfo = $test[0]; - $route = $test[1]; - $params = isset($test[2]) ? $test[2] : []; - $result = $rule->parseRequest($manager, $request); - if ($route === false) { - $this->assertFalse($result, "Test#$i-$j: $name"); - } else { - $this->assertEquals([$route, $params], $result, "Test#$i-$j: $name"); - } - } - } - } + public function testParseRequest() + { + $manager = new UrlManager(['cache' => null]); + $request = new Request(['hostInfo' => 'http://en.example.com']); + $suites = $this->getTestsForParseRequest(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + $request->pathInfo = $test[0]; + $route = $test[1]; + $params = isset($test[2]) ? $test[2] : []; + $result = $rule->parseRequest($manager, $request); + if ($route === false) { + $this->assertFalse($result, "Test#$i-$j: $name"); + } else { + $this->assertEquals([$route, $params], $result, "Test#$i-$j: $name"); + } + } + } + } - protected function getTestsForCreateUrl() - { - // structure of each test - // message for the test - // config for the URL rule - // list of inputs and outputs - // route - // params - // expected output - return [ - [ - 'empty pattern', - [ - 'pattern' => '', - 'route' => 'post/index', - ], - [ - ['post/index', [], ''], - ['comment/index', [], false], - ['post/index', ['page' => 1], '?page=1'], - ], - ], - [ - 'without param', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - ], - [ - ['post/index', [], 'posts'], - ['comment/index', [], false], - ['post/index', ['page' => 1], 'posts?page=1'], - ], - ], - [ - 'parsing only', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - 'mode' => UrlRule::PARSING_ONLY, - ], - [ - ['post/index', [], false], - ], - ], - [ - 'with param', - [ - 'pattern' => 'post/<page>', - 'route' => 'post/index', - ], - [ - ['post/index', [], false], - ['comment/index', [], false], - ['post/index', ['page' => 1], 'post/1'], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/1?tag=a'], - ], - ], - [ - 'with param requirement', - [ - 'pattern' => 'post/<page:\d+>', - 'route' => 'post/index', - ], - [ - ['post/index', ['page' => 'abc'], false], - ['post/index', ['page' => 1], 'post/1'], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/1?tag=a'], - ], - ], - [ - 'with multiple params', - [ - 'pattern' => 'post/<page:\d+>-<tag>', - 'route' => 'post/index', - ], - [ - ['post/index', ['page' => '1abc'], false], - ['post/index', ['page' => 1], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/1-a'], - ], - ], - [ - 'with optional param', - [ - 'pattern' => 'post/<page:\d+>/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/a'], - ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2/a'], - ], - ], - [ - 'with optional param not in pattern', - [ - 'pattern' => 'post/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 2, 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/a'], - ], - ], - [ - 'multiple optional params', - [ - 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', - 'route' => 'post/index', - 'defaults' => ['page' => 1, 'sort' => 'yes'], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a', 'sort' => 'YES'], false], - ['post/index', ['page' => 1, 'tag' => 'a', 'sort' => 'yes'], 'post/a'], - ['post/index', ['page' => 2, 'tag' => 'a', 'sort' => 'yes'], 'post/2/a'], - ['post/index', ['page' => 2, 'tag' => 'a', 'sort' => 'no'], 'post/2/a/no'], - ['post/index', ['page' => 1, 'tag' => 'a', 'sort' => 'no'], 'post/a/no'], - ], - ], - [ - 'optional param and required param separated by dashes', - [ - 'pattern' => 'post/<page:\d+>-<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/-a'], - ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2-a'], - ], - ], - [ - 'optional param at the end', - [ - 'pattern' => 'post/<tag>/<page:\d+>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/a'], - ['post/index', ['page' => 2, 'tag' => 'a'], 'post/a/2'], - ], - ], - [ - 'consecutive optional params', - [ - 'pattern' => 'post/<page:\d+>/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1, 'tag' => 'a'], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post'], - ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2'], - ['post/index', ['page' => 1, 'tag' => 'b'], 'post/b'], - ['post/index', ['page' => 2, 'tag' => 'b'], 'post/2/b'], - ], - ], - [ - 'consecutive optional params separated by dash', - [ - 'pattern' => 'post/<page:\d+>-<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1, 'tag' => 'a'], - ], - [ - ['post/index', ['page' => 1], false], - ['post/index', ['page' => '1abc', 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a'], 'post/-'], - ['post/index', ['page' => 1, 'tag' => 'b'], 'post/-b'], - ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2-'], - ['post/index', ['page' => 2, 'tag' => 'b'], 'post/2-b'], - ], - ], - [ - 'route has parameters', - [ - 'pattern' => '<controller>/<action>', - 'route' => '<controller>/<action>', - 'defaults' => [], - ], - [ - ['post/index', ['page' => 1], 'post/index?page=1'], - ['module/post/index', [], false], - ], - ], - [ - 'route has parameters with regex', - [ - 'pattern' => '<controller:post|comment>/<action>', - 'route' => '<controller>/<action>', - 'defaults' => [], - ], - [ - ['post/index', ['page' => 1], 'post/index?page=1'], - ['comment/index', ['page' => 1], 'comment/index?page=1'], - ['test/index', ['page' => 1], false], - ['post', [], false], - ['module/post/index', [], false], - ['post/index', ['controller' => 'comment'], 'post/index?controller=comment'], - ], - ], - [ - 'route has default parameter', - [ - 'pattern' => '<controller:post|comment>/<action>', - 'route' => '<controller>/<action>', - 'defaults' => ['action' => 'index'], - ], - [ - ['post/view', ['page' => 1], 'post/view?page=1'], - ['comment/view', ['page' => 1], 'comment/view?page=1'], - ['test/view', ['page' => 1], false], - ['test/index', ['page' => 1], false], - ['post/index', ['page' => 1], 'post?page=1'], - ], - ], - [ - 'empty pattern with suffix', - [ - 'pattern' => '', - 'route' => 'post/index', - 'suffix' => '.html', - ], - [ - ['post/index', [], ''], - ['comment/index', [], false], - ['post/index', ['page' => 1], '?page=1'], - ], - ], - [ - 'regular pattern with suffix', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - 'suffix' => '.html', - ], - [ - ['post/index', [], 'posts.html'], - ['comment/index', [], false], - ['post/index', ['page' => 1], 'posts.html?page=1'], - ], - ], - [ - 'empty pattern with slash suffix', - [ - 'pattern' => '', - 'route' => 'post/index', - 'suffix' => '/', - ], - [ - ['post/index', [], ''], - ['comment/index', [], false], - ['post/index', ['page' => 1], '?page=1'], - ], - ], - [ - 'regular pattern with slash suffix', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - 'suffix' => '/', - ], - [ - ['post/index', [], 'posts/'], - ['comment/index', [], false], - ['post/index', ['page' => 1], 'posts/?page=1'], - ], - ], - [ - 'with host info', - [ - 'pattern' => 'post/<page:\d+>/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - 'host' => 'http://<lang:en|fr>.example.com', - ], - [ - ['post/index', ['page' => 1, 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a', 'lang' => 'en'], 'http://en.example.com/post/a'], - ], - ], - [ - 'with host info in pattern', - [ - 'pattern' => 'http://<lang:en|fr>.example.com/post/<page:\d+>/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/index', ['page' => 1, 'tag' => 'a'], false], - ['post/index', ['page' => 1, 'tag' => 'a', 'lang' => 'en'], 'http://en.example.com/post/a'], - ], - ], - ]; - } + protected function getTestsForCreateUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // route + // params + // expected output + return [ + [ + 'empty pattern', + [ + 'pattern' => '', + 'route' => 'post/index', + ], + [ + ['post/index', [], ''], + ['comment/index', [], false], + ['post/index', ['page' => 1], '?page=1'], + ], + ], + [ + 'without param', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + ], + [ + ['post/index', [], 'posts'], + ['comment/index', [], false], + ['post/index', ['page' => 1], 'posts?page=1'], + ], + ], + [ + 'parsing only', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::PARSING_ONLY, + ], + [ + ['post/index', [], false], + ], + ], + [ + 'with param', + [ + 'pattern' => 'post/<page>', + 'route' => 'post/index', + ], + [ + ['post/index', [], false], + ['comment/index', [], false], + ['post/index', ['page' => 1], 'post/1'], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/1?tag=a'], + ], + ], + [ + 'with param requirement', + [ + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + ], + [ + ['post/index', ['page' => 'abc'], false], + ['post/index', ['page' => 1], 'post/1'], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/1?tag=a'], + ], + ], + [ + 'with multiple params', + [ + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + ], + [ + ['post/index', ['page' => '1abc'], false], + ['post/index', ['page' => 1], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/1-a'], + ], + ], + [ + 'with optional param', + [ + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/a'], + ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2/a'], + ], + ], + [ + 'with optional param not in pattern', + [ + 'pattern' => 'post/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 2, 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/a'], + ], + ], + [ + 'multiple optional params', + [ + 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', + 'route' => 'post/index', + 'defaults' => ['page' => 1, 'sort' => 'yes'], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a', 'sort' => 'YES'], false], + ['post/index', ['page' => 1, 'tag' => 'a', 'sort' => 'yes'], 'post/a'], + ['post/index', ['page' => 2, 'tag' => 'a', 'sort' => 'yes'], 'post/2/a'], + ['post/index', ['page' => 2, 'tag' => 'a', 'sort' => 'no'], 'post/2/a/no'], + ['post/index', ['page' => 1, 'tag' => 'a', 'sort' => 'no'], 'post/a/no'], + ], + ], + [ + 'optional param and required param separated by dashes', + [ + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/-a'], + ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2-a'], + ], + ], + [ + 'optional param at the end', + [ + 'pattern' => 'post/<tag>/<page:\d+>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/a'], + ['post/index', ['page' => 2, 'tag' => 'a'], 'post/a/2'], + ], + ], + [ + 'consecutive optional params', + [ + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1, 'tag' => 'a'], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post'], + ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2'], + ['post/index', ['page' => 1, 'tag' => 'b'], 'post/b'], + ['post/index', ['page' => 2, 'tag' => 'b'], 'post/2/b'], + ], + ], + [ + 'consecutive optional params separated by dash', + [ + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1, 'tag' => 'a'], + ], + [ + ['post/index', ['page' => 1], false], + ['post/index', ['page' => '1abc', 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a'], 'post/-'], + ['post/index', ['page' => 1, 'tag' => 'b'], 'post/-b'], + ['post/index', ['page' => 2, 'tag' => 'a'], 'post/2-'], + ['post/index', ['page' => 2, 'tag' => 'b'], 'post/2-b'], + ], + ], + [ + 'route has parameters', + [ + 'pattern' => '<controller>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => [], + ], + [ + ['post/index', ['page' => 1], 'post/index?page=1'], + ['module/post/index', [], false], + ], + ], + [ + 'route has parameters with regex', + [ + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => [], + ], + [ + ['post/index', ['page' => 1], 'post/index?page=1'], + ['comment/index', ['page' => 1], 'comment/index?page=1'], + ['test/index', ['page' => 1], false], + ['post', [], false], + ['module/post/index', [], false], + ['post/index', ['controller' => 'comment'], 'post/index?controller=comment'], + ], + ], + [ + 'route has default parameter', + [ + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => ['action' => 'index'], + ], + [ + ['post/view', ['page' => 1], 'post/view?page=1'], + ['comment/view', ['page' => 1], 'comment/view?page=1'], + ['test/view', ['page' => 1], false], + ['test/index', ['page' => 1], false], + ['post/index', ['page' => 1], 'post?page=1'], + ], + ], + [ + 'empty pattern with suffix', + [ + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ], + [ + ['post/index', [], ''], + ['comment/index', [], false], + ['post/index', ['page' => 1], '?page=1'], + ], + ], + [ + 'regular pattern with suffix', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ], + [ + ['post/index', [], 'posts.html'], + ['comment/index', [], false], + ['post/index', ['page' => 1], 'posts.html?page=1'], + ], + ], + [ + 'empty pattern with slash suffix', + [ + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ], + [ + ['post/index', [], ''], + ['comment/index', [], false], + ['post/index', ['page' => 1], '?page=1'], + ], + ], + [ + 'regular pattern with slash suffix', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ], + [ + ['post/index', [], 'posts/'], + ['comment/index', [], false], + ['post/index', ['page' => 1], 'posts/?page=1'], + ], + ], + [ + 'with host info', + [ + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + 'host' => 'http://<lang:en|fr>.example.com', + ], + [ + ['post/index', ['page' => 1, 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a', 'lang' => 'en'], 'http://en.example.com/post/a'], + ], + ], + [ + 'with host info in pattern', + [ + 'pattern' => 'http://<lang:en|fr>.example.com/post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/index', ['page' => 1, 'tag' => 'a'], false], + ['post/index', ['page' => 1, 'tag' => 'a', 'lang' => 'en'], 'http://en.example.com/post/a'], + ], + ], + ]; + } - protected function getTestsForParseRequest() - { - // structure of each test - // message for the test - // config for the URL rule - // list of inputs and outputs - // pathInfo - // expected route, or false if the rule doesn't apply - // expected params, or not set if empty - return [ - [ - 'empty pattern', - [ - 'pattern' => '', - 'route' => 'post/index', - ], - [ - ['', 'post/index'], - ['a', false], - ], - ], - [ - 'without param', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - ], - [ - ['posts', 'post/index'], - ['a', false], - ], - ], - [ - 'with dot', // https://github.com/yiisoft/yii/issues/2945 - [ - 'pattern' => 'posts.html', - 'route' => 'post/index', - ], - [ - ['posts.html', 'post/index'], - ['postsahtml', false], - ], - ], - [ - 'creation only', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - 'mode' => UrlRule::CREATION_ONLY, - ], - [ - ['posts', false], - ], - ], - [ - 'with param', - [ - 'pattern' => 'post/<page>', - 'route' => 'post/index', - ], - [ - ['post/1', 'post/index', ['page' => '1']], - ['post/a', 'post/index', ['page' => 'a']], - ['post', false], - ['posts', false], - ], - ], - [ - 'with param requirement', - [ - 'pattern' => 'post/<page:\d+>', - 'route' => 'post/index', - ], - [ - ['post/1', 'post/index', ['page' => '1']], - ['post/a', false], - ['post/1/a', false], - ], - ], - [ - 'with multiple params', - [ - 'pattern' => 'post/<page:\d+>-<tag>', - 'route' => 'post/index', - ], - [ - ['post/1-a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/a', false], - ['post/1', false], - ['post/1/a', false], - ], - ], - [ - 'with optional param', - [ - 'pattern' => 'post/<page:\d+>/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/1/a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/2/a', 'post/index', ['page' => '2', 'tag' => 'a']], - ['post/a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/1', 'post/index', ['page' => '1', 'tag' => '1']], - ], - ], - [ - 'with optional param not in pattern', - [ - 'pattern' => 'post/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/1', 'post/index', ['page' => '1', 'tag' => '1']], - ['post', false], - ], - ], - [ - 'multiple optional params', - [ - 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', - 'route' => 'post/index', - 'defaults' => ['page' => 1, 'sort' => 'yes'], - ], - [ - ['post/1/a/yes', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'yes']], - ['post/1/a/no', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'no']], - ['post/2/a/no', 'post/index', ['page' => '2', 'tag' => 'a', 'sort' => 'no']], - ['post/2/a', 'post/index', ['page' => '2', 'tag' => 'a', 'sort' => 'yes']], - ['post/a/no', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'no']], - ['post/a', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'yes']], - ['post', false], - ], - ], - [ - 'optional param and required param separated by dashes', - [ - 'pattern' => 'post/<page:\d+>-<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/1-a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/2-a', 'post/index', ['page' => '2', 'tag' => 'a']], - ['post/-a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/a', false], - ['post-a', false], - ], - ], - [ - 'optional param at the end', - [ - 'pattern' => 'post/<tag>/<page:\d+>', - 'route' => 'post/index', - 'defaults' => ['page' => 1], - ], - [ - ['post/a/1', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/a/2', 'post/index', ['page' => '2', 'tag' => 'a']], - ['post/a', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/2', 'post/index', ['page' => '1', 'tag' => '2']], - ['post', false], - ], - ], - [ - 'consecutive optional params', - [ - 'pattern' => 'post/<page:\d+>/<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1, 'tag' => 'a'], - ], - [ - ['post/2/b', 'post/index', ['page' => '2', 'tag' => 'b']], - ['post/2', 'post/index', ['page' => '2', 'tag' => 'a']], - ['post', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post/b', 'post/index', ['page' => '1', 'tag' => 'b']], - ['post//b', false], - ], - ], - [ - 'consecutive optional params separated by dash', - [ - 'pattern' => 'post/<page:\d+>-<tag>', - 'route' => 'post/index', - 'defaults' => ['page' => 1, 'tag' => 'a'], - ], - [ - ['post/2-b', 'post/index', ['page' => '2', 'tag' => 'b']], - ['post/2-', 'post/index', ['page' => '2', 'tag' => 'a']], - ['post/-b', 'post/index', ['page' => '1', 'tag' => 'b']], - ['post/-', 'post/index', ['page' => '1', 'tag' => 'a']], - ['post', false], - ], - ], - [ - 'route has parameters', - [ - 'pattern' => '<controller>/<action>', - 'route' => '<controller>/<action>', - 'defaults' => [], - ], - [ - ['post/index', 'post/index'], - ['module/post/index', false], - ], - ], - [ - 'route has parameters with regex', - [ - 'pattern' => '<controller:post|comment>/<action>', - 'route' => '<controller>/<action>', - 'defaults' => [], - ], - [ - ['post/index', 'post/index'], - ['comment/index', 'comment/index'], - ['test/index', false], - ['post', false], - ['module/post/index', false], - ], - ], - [ - 'route has default parameter', - [ - 'pattern' => '<controller:post|comment>/<action>', - 'route' => '<controller>/<action>', - 'defaults' => ['action' => 'index'], - ], - [ - ['post/view', 'post/view'], - ['comment/view', 'comment/view'], - ['test/view', false], - ['post', 'post/index'], - ['posts', false], - ['test', false], - ['index', false], - ], - ], - [ - 'empty pattern with suffix', - [ - 'pattern' => '', - 'route' => 'post/index', - 'suffix' => '.html', - ], - [ - ['', 'post/index'], - ['.html', false], - ['a.html', false], - ], - ], - [ - 'regular pattern with suffix', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - 'suffix' => '.html', - ], - [ - ['posts.html', 'post/index'], - ['posts', false], - ['posts.HTML', false], - ['a.html', false], - ['a', false], - ], - ], - [ - 'empty pattern with slash suffix', - [ - 'pattern' => '', - 'route' => 'post/index', - 'suffix' => '/', - ], - [ - ['', 'post/index'], - ['a', false], - ], - ], - [ - 'regular pattern with slash suffix', - [ - 'pattern' => 'posts', - 'route' => 'post/index', - 'suffix' => '/', - ], - [ - ['posts/', 'post/index'], - ['posts', false], - ['a', false], - ], - ], - [ - 'with host info', - [ - 'pattern' => 'post/<page:\d+>', - 'route' => 'post/index', - 'host' => 'http://<lang:en|fr>.example.com', - ], - [ - ['post/1', 'post/index', ['page' => '1', 'lang' => 'en']], - ['post/a', false], - ['post/1/a', false], - ], - ], - [ - 'with host info in pattern', - [ - 'pattern' => 'http://<lang:en|fr>.example.com/post/<page:\d+>', - 'route' => 'post/index', - ], - [ - ['post/1', 'post/index', ['page' => '1', 'lang' => 'en']], - ['post/a', false], - ['post/1/a', false], - ], - ], - ]; - } + protected function getTestsForParseRequest() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // pathInfo + // expected route, or false if the rule doesn't apply + // expected params, or not set if empty + return [ + [ + 'empty pattern', + [ + 'pattern' => '', + 'route' => 'post/index', + ], + [ + ['', 'post/index'], + ['a', false], + ], + ], + [ + 'without param', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + ], + [ + ['posts', 'post/index'], + ['a', false], + ], + ], + [ + 'with dot', // https://github.com/yiisoft/yii/issues/2945 + [ + 'pattern' => 'posts.html', + 'route' => 'post/index', + ], + [ + ['posts.html', 'post/index'], + ['postsahtml', false], + ], + ], + [ + 'creation only', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::CREATION_ONLY, + ], + [ + ['posts', false], + ], + ], + [ + 'with param', + [ + 'pattern' => 'post/<page>', + 'route' => 'post/index', + ], + [ + ['post/1', 'post/index', ['page' => '1']], + ['post/a', 'post/index', ['page' => 'a']], + ['post', false], + ['posts', false], + ], + ], + [ + 'with param requirement', + [ + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + ], + [ + ['post/1', 'post/index', ['page' => '1']], + ['post/a', false], + ['post/1/a', false], + ], + ], + [ + 'with multiple params', + [ + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + ], + [ + ['post/1-a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/a', false], + ['post/1', false], + ['post/1/a', false], + ], + ], + [ + 'with optional param', + [ + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/1/a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/2/a', 'post/index', ['page' => '2', 'tag' => 'a']], + ['post/a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/1', 'post/index', ['page' => '1', 'tag' => '1']], + ], + ], + [ + 'with optional param not in pattern', + [ + 'pattern' => 'post/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/1', 'post/index', ['page' => '1', 'tag' => '1']], + ['post', false], + ], + ], + [ + 'multiple optional params', + [ + 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', + 'route' => 'post/index', + 'defaults' => ['page' => 1, 'sort' => 'yes'], + ], + [ + ['post/1/a/yes', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'yes']], + ['post/1/a/no', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'no']], + ['post/2/a/no', 'post/index', ['page' => '2', 'tag' => 'a', 'sort' => 'no']], + ['post/2/a', 'post/index', ['page' => '2', 'tag' => 'a', 'sort' => 'yes']], + ['post/a/no', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'no']], + ['post/a', 'post/index', ['page' => '1', 'tag' => 'a', 'sort' => 'yes']], + ['post', false], + ], + ], + [ + 'optional param and required param separated by dashes', + [ + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/1-a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/2-a', 'post/index', ['page' => '2', 'tag' => 'a']], + ['post/-a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/a', false], + ['post-a', false], + ], + ], + [ + 'optional param at the end', + [ + 'pattern' => 'post/<tag>/<page:\d+>', + 'route' => 'post/index', + 'defaults' => ['page' => 1], + ], + [ + ['post/a/1', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/a/2', 'post/index', ['page' => '2', 'tag' => 'a']], + ['post/a', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/2', 'post/index', ['page' => '1', 'tag' => '2']], + ['post', false], + ], + ], + [ + 'consecutive optional params', + [ + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1, 'tag' => 'a'], + ], + [ + ['post/2/b', 'post/index', ['page' => '2', 'tag' => 'b']], + ['post/2', 'post/index', ['page' => '2', 'tag' => 'a']], + ['post', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post/b', 'post/index', ['page' => '1', 'tag' => 'b']], + ['post//b', false], + ], + ], + [ + 'consecutive optional params separated by dash', + [ + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => ['page' => 1, 'tag' => 'a'], + ], + [ + ['post/2-b', 'post/index', ['page' => '2', 'tag' => 'b']], + ['post/2-', 'post/index', ['page' => '2', 'tag' => 'a']], + ['post/-b', 'post/index', ['page' => '1', 'tag' => 'b']], + ['post/-', 'post/index', ['page' => '1', 'tag' => 'a']], + ['post', false], + ], + ], + [ + 'route has parameters', + [ + 'pattern' => '<controller>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => [], + ], + [ + ['post/index', 'post/index'], + ['module/post/index', false], + ], + ], + [ + 'route has parameters with regex', + [ + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => [], + ], + [ + ['post/index', 'post/index'], + ['comment/index', 'comment/index'], + ['test/index', false], + ['post', false], + ['module/post/index', false], + ], + ], + [ + 'route has default parameter', + [ + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => ['action' => 'index'], + ], + [ + ['post/view', 'post/view'], + ['comment/view', 'comment/view'], + ['test/view', false], + ['post', 'post/index'], + ['posts', false], + ['test', false], + ['index', false], + ], + ], + [ + 'empty pattern with suffix', + [ + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ], + [ + ['', 'post/index'], + ['.html', false], + ['a.html', false], + ], + ], + [ + 'regular pattern with suffix', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ], + [ + ['posts.html', 'post/index'], + ['posts', false], + ['posts.HTML', false], + ['a.html', false], + ['a', false], + ], + ], + [ + 'empty pattern with slash suffix', + [ + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ], + [ + ['', 'post/index'], + ['a', false], + ], + ], + [ + 'regular pattern with slash suffix', + [ + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ], + [ + ['posts/', 'post/index'], + ['posts', false], + ['a', false], + ], + ], + [ + 'with host info', + [ + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + 'host' => 'http://<lang:en|fr>.example.com', + ], + [ + ['post/1', 'post/index', ['page' => '1', 'lang' => 'en']], + ['post/a', false], + ['post/1/a', false], + ], + ], + [ + 'with host info in pattern', + [ + 'pattern' => 'http://<lang:en|fr>.example.com/post/<page:\d+>', + 'route' => 'post/index', + ], + [ + ['post/1', 'post/index', ['page' => '1', 'lang' => 'en']], + ['post/a', false], + ['post/1/a', false], + ], + ], + ]; + } } diff --git a/tests/unit/framework/web/XmlResponseFormatterTest.php b/tests/unit/framework/web/XmlResponseFormatterTest.php index bf1aa294d73..efb26f85bd0 100644 --- a/tests/unit/framework/web/XmlResponseFormatterTest.php +++ b/tests/unit/framework/web/XmlResponseFormatterTest.php @@ -13,14 +13,14 @@ class Post extends Object { - public $id; - public $title; + public $id; + public $title; - public function __construct($id, $title) - { - $this->id = $id; - $this->title = $title; - } + public function __construct($id, $title) + { + $this->id = $id; + $this->title = $title; + } } /** @@ -31,108 +31,108 @@ public function __construct($id, $title) */ class XmlResponseFormatterTest extends \yiiunit\TestCase { - /** - * @var Response - */ - public $response; - /** - * @var XmlResponseFormatter - */ - public $formatter; + /** + * @var Response + */ + public $response; + /** + * @var XmlResponseFormatter + */ + public $formatter; - protected function setUp() - { - $this->mockApplication(); - $this->response = new Response; - $this->formatter = new XmlResponseFormatter; - } + protected function setUp() + { + $this->mockApplication(); + $this->response = new Response; + $this->formatter = new XmlResponseFormatter; + } - /** - * @param mixed $data the data to be formatted - * @param string $xml the expected XML body - * @dataProvider formatScalarDataProvider - */ - public function testFormatScalar($data, $xml) - { - $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; - $this->response->data = $data; - $this->formatter->format($this->response); - $this->assertEquals($head . $xml, $this->response->content); - } - - public function formatScalarDataProvider() - { - return [ - [null, "<response></response>\n"], - [1, "<response>1</response>\n"], - ['abc', "<response>abc</response>\n"], - [true, "<response>1</response>\n"], - ["<>", "<response><></response>\n"], - ]; - } + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatScalarDataProvider + */ + public function testFormatScalar($data, $xml) + { + $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } - /** - * @param mixed $data the data to be formatted - * @param string $xml the expected XML body - * @dataProvider formatArrayDataProvider - */ - public function testFormatArrays($data, $xml) - { - $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; - $this->response->data = $data; - $this->formatter->format($this->response); - $this->assertEquals($head . $xml, $this->response->content); - } + public function formatScalarDataProvider() + { + return [ + [null, "<response></response>\n"], + [1, "<response>1</response>\n"], + ['abc', "<response>abc</response>\n"], + [true, "<response>1</response>\n"], + ["<>", "<response><></response>\n"], + ]; + } - public function formatArrayDataProvider() - { - return [ - [[], "<response/>\n"], - [[1, 'abc'], "<response><item>1</item><item>abc</item></response>\n"], - [[ - 'a' => 1, - 'b' => 'abc', - ], "<response><a>1</a><b>abc</b></response>\n"], - [[ - 1, - 'abc', - [2, 'def'], - true, - ], "<response><item>1</item><item>abc</item><item><item>2</item><item>def</item></item><item>1</item></response>\n"], - [[ - 'a' => 1, - 'b' => 'abc', - 'c' => [2, '<>'], - true, - ], "<response><a>1</a><b>abc</b><c><item>2</item><item><></item></c><item>1</item></response>\n"], - ]; - } + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatArrayDataProvider + */ + public function testFormatArrays($data, $xml) + { + $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } - /** - * @param mixed $data the data to be formatted - * @param string $xml the expected XML body - * @dataProvider formatObjectDataProvider - */ - public function testFormatObjects($data, $xml) - { - $head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; - $this->response->data = $data; - $this->formatter->format($this->response); - $this->assertEquals($head . $xml, $this->response->content); - } + public function formatArrayDataProvider() + { + return [ + [[], "<response/>\n"], + [[1, 'abc'], "<response><item>1</item><item>abc</item></response>\n"], + [[ + 'a' => 1, + 'b' => 'abc', + ], "<response><a>1</a><b>abc</b></response>\n"], + [[ + 1, + 'abc', + [2, 'def'], + true, + ], "<response><item>1</item><item>abc</item><item><item>2</item><item>def</item></item><item>1</item></response>\n"], + [[ + 'a' => 1, + 'b' => 'abc', + 'c' => [2, '<>'], + true, + ], "<response><a>1</a><b>abc</b><c><item>2</item><item><></item></c><item>1</item></response>\n"], + ]; + } - public function formatObjectDataProvider() - { - return [ - [new Post(123, 'abc'), "<response><Post><id>123</id><title>abc\n"], - [[ - new Post(123, 'abc'), - new Post(456, 'def'), - ], "123abc456def\n"], - [[ - new Post(123, '<>'), - 'a' => new Post(456, 'def'), - ], "123<>456def\n"], - ]; - } + /** + * @param mixed $data the data to be formatted + * @param string $xml the expected XML body + * @dataProvider formatObjectDataProvider + */ + public function testFormatObjects($data, $xml) + { + $head = "\n"; + $this->response->data = $data; + $this->formatter->format($this->response); + $this->assertEquals($head . $xml, $this->response->content); + } + + public function formatObjectDataProvider() + { + return [ + [new Post(123, 'abc'), "123abc\n"], + [[ + new Post(123, 'abc'), + new Post(456, 'def'), + ], "123abc456def\n"], + [[ + new Post(123, '<>'), + 'a' => new Post(456, 'def'), + ], "123<>456def\n"], + ]; + } } diff --git a/tests/unit/framework/widgets/ActiveFormTest.php b/tests/unit/framework/widgets/ActiveFormTest.php index 6bc90745360..784b4f2ec16 100644 --- a/tests/unit/framework/widgets/ActiveFormTest.php +++ b/tests/unit/framework/widgets/ActiveFormTest.php @@ -13,41 +13,41 @@ */ class ActiveFormTest extends \yiiunit\TestCase { - protected function setUp() - { - $this->mockApplication(); - } - - public function testBooleanAttributes() - { - $o = ['template' => '{input}']; - - $model = new DynamicModel(['name']); - ob_start(); - $form = new ActiveForm(['action' => './']); - ob_end_clean(); - - $this->assertEquals(<<mockApplication(); + } + + public function testBooleanAttributes() + { + $o = ['template' => '{input}']; + + $model = new DynamicModel(['name']); + ob_start(); + $form = new ActiveForm(['action' => './']); + ob_end_clean(); + + $this->assertEquals(<< EOF , (string) $form->field($model, 'name', $o)->input('email', ['required' => true])); - $this->assertEquals(<<assertEquals(<< EOF - , (string) $form->field($model, 'name', $o)->input('email', ['required' => false])); + , (string) $form->field($model, 'name', $o)->input('email', ['required' => false])); - $this->assertEquals(<<assertEquals(<< EOF - , (string) $form->field($model, 'name', $o)->input('email', ['required' => 'test'])); + , (string) $form->field($model, 'name', $o)->input('email', ['required' => 'test'])); - } + } } diff --git a/tests/unit/framework/widgets/SpacelessTest.php b/tests/unit/framework/widgets/SpacelessTest.php index 8f7ae612f43..ae41b4c0249 100644 --- a/tests/unit/framework/widgets/SpacelessTest.php +++ b/tests/unit/framework/widgets/SpacelessTest.php @@ -9,33 +9,33 @@ */ class SpacelessTest extends \yiiunit\TestCase { - public function testWidget() - { - ob_start(); - ob_implicit_flush(false); - - echo "\n"; - - Spaceless::begin(); - echo "\t
        \n"; - - Spaceless::begin(); - echo "\t\t
        \n"; - echo "\t\t\t

        This is a left bar!

        \n"; - echo "\t\t
        \n\n"; - echo "\t\t
        \n"; - echo "\t\t\t

        This is a right bar!

        \n"; - echo "\t\t
        \n"; - Spaceless::end(); - - echo "\t
        \n"; - Spaceless::end(); - - echo "\t

        Bye!

        \n"; - echo "\n"; - - $expected = "\n

        This is a left bar!

        ". - "

        This is a right bar!

        \t

        Bye!

        \n\n"; - $this->assertEquals($expected, ob_get_clean()); - } + public function testWidget() + { + ob_start(); + ob_implicit_flush(false); + + echo "\n"; + + Spaceless::begin(); + echo "\t
        \n"; + + Spaceless::begin(); + echo "\t\t
        \n"; + echo "\t\t\t

        This is a left bar!

        \n"; + echo "\t\t
        \n\n"; + echo "\t\t
        \n"; + echo "\t\t\t

        This is a right bar!

        \n"; + echo "\t\t
        \n"; + Spaceless::end(); + + echo "\t
        \n"; + Spaceless::end(); + + echo "\t

        Bye!

        \n"; + echo "\n"; + + $expected = "\n

        This is a left bar!

        ". + "

        This is a right bar!

        \t

        Bye!

        \n\n"; + $this->assertEquals($expected, ob_get_clean()); + } } diff --git a/tests/web/app/protected/controllers/SiteController.php b/tests/web/app/protected/controllers/SiteController.php index b79f3a6ce62..8f6f027800e 100644 --- a/tests/web/app/protected/controllers/SiteController.php +++ b/tests/web/app/protected/controllers/SiteController.php @@ -4,27 +4,27 @@ class DefaultController extends \yii\web\Controller { - public function actionIndex() - { - echo 'hello world'; - } - - public function actionForm() - { - echo Html::beginForm(); - echo Html::checkboxList('test', [ - 'value 1' => 'item 1', - 'value 2' => 'item 2', - 'value 3' => 'item 3', - ], isset($_POST['test']) ? $_POST['test'] : null, - function ($index, $label, $name, $value, $checked) { - return Html::label( - $label . ' ' . Html::checkbox($name, $value, $checked), - null, ['class' => 'inline checkbox'] - ); - }); - echo Html::submitButton(); - echo Html::endForm(); - print_r($_POST); - } + public function actionIndex() + { + echo 'hello world'; + } + + public function actionForm() + { + echo Html::beginForm(); + echo Html::checkboxList('test', [ + 'value 1' => 'item 1', + 'value 2' => 'item 2', + 'value 3' => 'item 3', + ], isset($_POST['test']) ? $_POST['test'] : null, + function ($index, $label, $name, $value, $checked) { + return Html::label( + $label . ' ' . Html::checkbox($name, $value, $checked), + null, ['class' => 'inline checkbox'] + ); + }); + echo Html::submitButton(); + echo Html::endForm(); + print_r($_POST); + } }