商業模式筆記

線上工具

Customer Segments (目標客層)

為誰創造價值? 誰是我們最重要的客戶?

Mass Market(廣泛市場營銷)

是指營銷者以相同的方式向市場上所有的消費者提供相同的產品和進行信息溝通,即大量生產、大量分銷和大量促銷。例如,亨利·福特向市場上推出著名的T型車時,就採用統一的設計和唯一的黑色款式。同樣,可口可樂一度只向整個市場供應一種可樂,以求吸引所有的消費者。

  廣泛市場營銷以市場的共性為基礎,忽略市場需求的差異,力圖以標準化的產品和分銷影響最廣泛的市場範圍,從而獲得最低的生產和營銷成本,得到較低的價格,或者較高的利潤。在商品不充足、消費個性不突出、或產品需求同質性高的情況下,廣泛市場營銷能夠有效地實現規模經濟,為企業所推崇。

Niche Market(利基市場)

指向那些被市場中的統治者/有絕對優勢的企業忽略的某些細分市場,利基市場是指企業選定一個很小的產品或服務領域,集中力量進入併成為領先者,從當地市場到全國再到全球,同時建立各種壁壘,逐漸形成持久的競爭優勢。

以圖畫隱喻來說明 Niche Market 與 Mass Market

Segmented(市場細分)

市場細分

Diversified(多元化經營)

多角化經營

Multi-Sided Platform(多邊市場)

雙邊市場是指一個具兩個不同客群的經濟平臺,這兩個客群能互相提供對方網路利益。若一組織創造價值的方法主要在於讓兩個(或多個)相關聯的不同客群能有直接的互動,則稱之為多邊平臺(MSP)[1]。雙邊市場可以在許多產業中出現,如信用卡(持卡人和商家)、健康維護組織(病人和醫生)、作業系統(終端用戶和開發人員)、夜店(男人尋找女人,且通常會買酒)、黃頁(廣告商和消費者)、家用遊戲機(玩家及遊戲開發商)、求職網站(求職者和徵才公司)、搜尋引擎(廣告商和用戶),及網際網路之類的電信網路等。採用雙邊市場的知名公司有美國運通、eBay、Facebook、美國商城、Match.com、Monster.com、Sony、Skype、Google等。每個客群都會從需求的規模經濟中得到好處。舉例而言,持卡人會喜歡能被較多商家接受的信用卡,而商家則會喜歡被較多人持有的信用卡。這在分析雞生蛋之類的競爭問題時特別有效,如VHS和Betamax之間的競爭。雙邊市場亦有助於解釋免費訂價策略,其中一方的客群可以免費使用此平臺,以吸引另一方客群的加入

Value Propositions (價值主張)

  • 我們傳遞什們價值給使用者?
  • 我們希望解決使用者的哪一個問題?
  • 我們打算提供怎樣的產品和服務給目標客層?
  • 我們滿足了哪些客戶需求?
  1. Newness:
    Some value propositions satisfy an entirely new set of needs that customers previously didn’t perceive because there was no similar offering. This is often, but not always, technology related. Examples of new value propositions include cell phones and ethical investment funds.

  2. Performance:
    Improving product or service performance is a common way to create value for customers. Think PCs and cameras, which are continually being made faster and more capable. As Clayton Christensen points out in his seminal book, the Innovator’s Dilemma, too often companies follow this method of value creation so blindly that eventually their products and services exceed the needs of their customers – making it harder to justify price increases and opening up yawning chasms of opportunity for low-cost, adequate offerings from new-to-market entrants.

  3. Customization:
    “Tailoring products and services to the specific needs of individual customers or customer segments creates value. In recent year,s the concepts of mass customization and customer co-creation have gained importance. This approach allows for customized products and services, while still taking advantage of economies of scale.”

  4. "Getting the Job done":
    Value can be created simply by helping a customer get jobs done. The authors use the example of Rolls-Royce, which not only manufactures jet engines for commercial aircraft, but also services them – freeing the airlines to focus on what they do best. Again, Christensen spends a lot of time in his books talking about JTBD – jobs to be done. Sometimes companies get so focused upon improving their products as an end unto itself that they lose track of the key tasks the customer needs to get done, the problems they need to solve.

  5. Design:
    This element of value creation is hard to quantify, but is becoming increasingly important. In consumer markets, design can be a very important part of the value proposition. Think Apple’s elegant iPhone, the fashion industry and automotive design, which appeals to customers at a more visceral level.

  6. Brand/Status
    Customers may find value in the simple act of using or displaying a specific brand. For example, wearing a Rolex watch or driving a Mercedes signfies wealth, and teens often have “in” brands that they must wear in order to be cool.

  7. Price
    Offering similar value at a lower price is a common way to satisfy the needs of price-sensitive customer segments. For example, no-frills airlines such as Southwest, Ryanair and easyJet have designed their entire business models around providing low-cost air travel. Another example cited by the authors is the growing popularity of free offers for various products and services. Another possibility is innovative pricing models, such as micro-financing.

  8. Cost Reduction
    “Helping customers to reduce costs is an important way to create value. Salesforce.com, for example, sells a hosted Customer Relationship Managment (CRM) application. This relieves buyers from the expense and trouble of having to buy, install and manage CRM software themselves.”

  9. Risk Reduction
    Customers value reducing the risks they incur when purchasing products or services. For example, at the beginning of the global recession, Hyundai made a pioneering offer to its U.S. customers: If you purchase a new Hyundai and subsequently lose your job, we’ll buy the car back from you. Not surprisingly, this program was a huge success for the Korean car manufacturer.
    (這個厲害= = )

  10. Accessibility
    “Making products and services available to customers who previously lacked access to them is another way to create value. This can result from business model innovation, new technologies or a combination of both. NetJets, for example, popularized the concept of fractional jet ownership. Using an innovative business model, NetJets offers individuals and corporations access to private jets, a service previously unaffordable to most customers. Mutual funds provide another example pf value creation through increased accessibility. This innovative financial product made it possible for even those with modest wealth to build diversified investment portfolios.”

  11. Convenience/Usability
    “Making things more convenient or easier to use can create substantial value. With iPod and iTunes, Apple offered customers unprecedented convenience searching, buying, downloading and listening to digital music. It now dominates the market.”

Customer Relationship (顧客關係)

Category 1: Personal assistance

This type of customer relationship is characterized by the human touch. Customers have the opportunity to interact with a sales representative while they are making their purchase decision or with a customer services representative for after sales services.

Category 2: Dedicated personal assistance

This type of relationship takes personal assistance to the next level by assigning dedicated customer care representatives to the customer. This kind of relationship takes some time and finesse to develop and is characterized by the representative knowing traits of the customer that he uses to customize the customer experience with the company. Banks will often assign a single point of contact to important customers with long-standing relationship with the bank and a high net worth.

Category 3: Self-service

The Do It Yourself model has been getting more and more popular as organizations seek cost cutting measures that will reflect in the prices given to customers. In this kind of relationship, the company provides all the tools a customer needs to service themselves.

Category 4: Automated services

Automated services are the next level of self-service by providing machinery and processes that increase the convenience for customers to perform services themselves. These kinds of services are usually much more customized and use a customer’s online and buying behavior to create a profile that is then used to provide suggestions to the customer to enhance his/ her shopping experience. Hence, automated services in many ways can be likened to personal assistance because of the customization that goes into the experience.

Category 5: Communities

In today’s social media driven environment, communities are a wonderful way for companies to understand their consumers, get insights into their habits, perspectives and create a platform in which customers can get together and share knowledge and experiences. In this way, not only does the company form a personal relationship with its customers, but these bonds are strengthened by the additional relationships customers form with one another. Glaxo SmithKline is an example of this kind of a relationship. When the company launched a new weight-loss drug, it gave customers a platform to form communities that helped it understand the problems that overweight people face as well as.

Category 6: Co-creation(web2.0)

Companies are increasingly changing the nature of the customer relationship by involving them in the design and even creation of the end product. This gives customers greater ownership over the product and service and often results in the creation of product or brand champions in the market. Amazon encourages customers to publish their book reviews on the web-site so readers can find people with similar tastes and evaluate what they thought of particular books before making purchase decisions. YouTube depends entirely on its customers to create the content that enables the website to boast being the largest video sharing website in the world.

Channels(通路)

  • 想要通過哪個通路接觸目標客戶層?
  • 我們目前是如何接觸他們的?
  • 我們是如何整合目前的通路的, 整合得如何?
  • 哪個運作的最好?
  • 哪個運作的最有效率?
  • 如何將通路和客戶的行為整合?(原文是customer routines, 不確定翻成這樣對不對...)

Phase 1: Awareness

How do we educate customers about the characteristics of the products and services we have? This is the marketing and advertising phase. It is how you let your customer know about your value proposition.

Phase 2: Evaluation

How can we aid customers in evaluating our Value Proposition? This is the promotion or ‘Try me before you buy me’ phase. The customer will evaluate, read about or use your product and form an opinion about it. A good company will educate customers with other competitors in the market and help them to evaluate their choices. In this way, you make your value proposition clearer to them and why you are a better option than your competitors.

Phase 3: Purchase

How can we help customers in buying their preferred product or service? This is the sales process and denoted the dollars exchanged for a particular goods and services.

Phase 4: Delivery

How do we deliver the promised value proposition to the customer? This is the fulfillment stage and defines how the product will reach the customer.

Phase 5: After Sales

How can we provide After Sales customer care and support? This phase creates Advocates for your products and services amongst your target segment. This stage provides a person for the customer to call when they have a problem or question about the product. The higher the value of the product, the more likely it is that he/ she will require After Sales support.

Revenue Streams 收益流

  • 哪些價值是我們的客戶真正願意付費取得的?
  • 我們的客戶目前付費取得的是?
  • 他們目前如何付費?
  • 他們會更偏好如何付費?
  • 每個收益流的金額?

WAYS TO GENERATE REVENUE STREAM

  1. Asset sale(實體商品的所有權轉移)

This kind of sale refers to the transfer of ownership rights of a physical product from the seller to the buyer. At Amazon.com ownership rights of a myriad of products such as books, music and electronics are sold to the buyers. Similarly, Honda sells the ownership rights of the cars it manufactures to the buyers after which the buyer has complete freedom to rent out, use or even total the car.

  1. Usage fee(使用費, 如理髮, 網路)

This kind of fee is usually charged by service providers to customers for the use of the service. Hence, an internet provider will probably charge a customer for using their line for a certain number of minutes during the day or month. A beautician may charge her customer according to the number and nature of treatments the customer undergoes while under her care.

  1. Subscription fees(健身房會員)

When a user requires long-term or continuous access to the products of a company, they pay a subscription fee. Hence, a gym may sell a yearly membership subscription to its customer. Cable providers may charge a subscription fee to its users based on the time for which they will pay upfront.

  1. Lending/ renting/ leasing(租車之類的服務)

Some organizations provide their customers with exclusive rights to their product for a limited amount of time for a set fee. Upon the end of this period, the organization regains ownership of the product. This kind of revenue model represents a number of advantages both for the company and the customer. The company enjoys recurring revenue from the customer for the mentioned period. On the other end of the coin, the customer has exclusive access to the product for the time he/ she require it without having to make a hefty investment. Hence, zipcar.com a popular car renting service in North America allows customers to rent their cars for a specified time period. This has become a very popular service in the cities it is available because it provides customers with the advantage of a car, without having to invest in buying one.

  1. Licensing(如windows, office)

Licensing is generally used when we are talking about products, services or ideas that fall under the parameter of intellectual property. This opens up a revenue stream for rights holders, who would otherwise have had to invest in manufacturing as well. It is common in the Technology industry for patent holders to license the use of patents to other companies and to charge a licensing fee for it.

  1. Brokerage fee(104, airbnb)

When a company acts as an intermediary to ease the communication and transaction between two or more parties, they charge a brokerage fee. An example of this is when a headhunting firm matches a candidate to an organization looking for a particular skill set. The firm usually charges a percentage of the gross salary from the organization, the candidate or both.

  1. Advertising(廣告)

Companies that earn a fee through promoting another organization, product or service, charge an advertising fee for their service. Traditionally this kind of revenue was common only in the advertising industry. However, in recent times, with the boom of the internet and e-commerce, many websites are also using this as a main revenue stream.

PRICING MECHANISM

Pricing mechanisms refer to the effect of the pricing of a product on its expected demand and supply. This is essentially a tool to match buyers to the sellers of a product. Each revenue stream in a business can have its individual pricing mechanism. The pricing mechanism selected has a significant impact on the revenues generated by the revenue stream in question. Pricing mechanisms can be divided into two types; a) fixed pricing and b) dynamic pricing.

  1. Fixed Pricing

This kind of pricing, as the name suggests, remains uniform due to the lack of variability in the inputs that go into the product.

  • Fixed List Pricing(製造商議定價目)

Fixed list pricing is the pricing mentioned by the manufacturer for a product, service or value proposition of an organization.

  • Product feature dependent(根據產品某幾項功能定價)

When a product has a number of value propositions important to the customer, it may be priced according to the amount of such features.

  • Customer segment dependent(根據客戶特點調整價格)

This kind of pricing takes the target customer segment and their various traits into account.

  • Volume dependent(買越多越便宜)

As the name suggests, the more quantity a customer purchases, typically the lower the price will be.

  1. Dynamic Pricing

This type of pricing changes according to the variables that go into the product as well as the conditions prevalent in the market.

  • Bargaining (共同議價)

This refers to when a price is negotiated between two or more parties. The outcome of the negotiation is dependent on who holds the power at the negotiation table as well as the relative skills of the parties involved.

  • Auctioning(競拍)

In this kind of dynamic pricing, the final price is dependent on the customers and their perception of the worth of the value the product or service holds. Usually, the product or service, goes through a process called bidding where target customers share what they are willing to pay for the product or service. The customer proposing the highest price gets the product or service.

  • Yield Management (限時優惠)

In yield management, the price is completely dependent on inventory and the time of purchase. It is a kind of variable pricing where the product or service has a time limit on it, and companies use customer intelligence to create revenues. Airlines and hotels are the most common adopters of this pricing model.

  • Real-time market(供需, 如石油)

In this kind of pricing, the onus of responsibility goes to the supply and demand for a particular product. The price keeps fluctuating depending on how much customers want the product and how much is available to sell.

Key Activities(關鍵活動)

  • 我們的價值主張需要哪些關鍵活動?
  • 我們的分銷渠道需要哪些關鍵活動? link
  • 顧客關係需要哪些關鍵活動?
  • 收益流需要哪些關鍵活動?

Production

These activities are generally a characteristic of manufacturing firms and entail the design, creation and delivery of significant quantities of the product.

For a company that manufactures and sells pantyhose, typical value propositions are listed below;

This pantyhose lasts longer and, therefore, saves consumers the money they would spend on frequent replacements.
It provides resistance, so feet don’t slip in heels.
The product is machine washable.
Easy to store packaging.
The Key activities would then be;

  1. Control of production and manufacturing;
  2. Manage website, online orders and the distribution of the product;
  3. Create a branding strategy;
  4. Marketing and promotion of the product;
  5. Product and packaging design. ##Problem-solving

Some consumers or customers have chronic problems. Organizations that list problem-solving as a main activity are usually aiming to find unique solutions to these individual problems. Consultancies, hospitals, and most service organizations typically are trying to solve customer problems uniquely. These organizations are characterized by lots of knowledge management and a focus on continuous learning.

Jiffy Lube is a chain of over 200 businesses in North America which offers oil change and other automotive services to its clientele. Hence, it is a service firm that aims to provide a solution to a recurring problem its target customers may have. Jiffy’s value propositions are:

Keep cars healthy;
Keep clothes clean and garages tidy;
Save customers’ time and help them avoid the hassle of their cars breaking down.
Based on these, the key activities performed at Jiffy can be as follows;

  1. Change the oil of cars;
  2. Perform other maintenance work;
  3. Promote their services to customers through upselling and other marketing activities.
  4. Such organizations will have detailed records on repair work done on the automobiles of their repeat customers and will be able to handle the car with full knowledge of its history, much like a doctor with a regular patient.

Platform/ network

A business model where the platform is a key resource usually has platform or network-related key activities. Networks, brands, and software can all be a part of a platform or network-related business.

Agile Enterprise Architecture or AEA offers its services to companies experiencing a surge of work or a cascade of models that need to be done within a limited amount of time. The company’s value propositions are as follows;

Low-cost architecture modeling;
Agile and available when needed by customers;
In the cloud;
Its environmentally friendly especially if the client company is willing to forego travel;
Efficient and effective;
Involves minimal risk.
Based on these the key activities for this organization are;

  1. Cloud-based architectural modeling as a service;
  2. Cloud-based enterprise architecture software as a service;
  3. Frequent health checks for the architecture to make sure it remains robust with changing environment.

Key Resources(關鍵資源)

  • 我們的價值主張需要哪些關鍵資源?
  • 我們的分銷渠道需要哪些關鍵資源? link
  • 顧客關係需要哪些關鍵資源?
  • 收益流需要哪些關鍵資源?
  1. Physical resources

Physical assets are tangible resources that a company uses to create its value proposition. These could include equipment, inventory, buildings, manufacturing plants and distribution networks that enable the business to function.

A microchip manufacturing company like Intel needs semi-conductor plants as a key resource and without adequate infrastructure available, the organization will fail to innovate and keep up with its business customer demands and needs.

  1. Intellectual resources

These are non-physical, intangible resources like brand, patents, IP, copyrights, and even partnerships. Customer lists, customer knowledge, and even your own people, represent a form of intellectual resource. Intellectual resources take a great deal of time and expenditure to develop. But once developed, they can offer unique advantages to the company. Nike and Sony are heavily dependent on their brand to sell their products to a customer segment that is devoted to the brand. Similarly, Microsoft and Adobe rely on software that have been tweaked and perfected over years of trial and error. Some businesses have very strong intellectual resources. Google is currently buying a patent library from Nortel to boost up its intellectual resources.

From the years 2000 and 2012, companies have increasingly realized the significance of intellectual resources. This can be seen through the visible increase in patents being filed in the United States. The number of patents filed by Google between 2011 and 2012 grew by 170%. Apple’s patents grew by 68% in the same time period. Hence, companies have started to see patents as a major driver of their business and growth.

  1. Human resources

Employees are often the most important and yet the most easily overlooked assets of an organization. Specifically for companies in the service industries or require a great deal of creativity and an extensive knowledge pool, human resources such as customer service representatives, software engineers or scientists are pivotal.

FedEx truck drivers are the human resources that combine with the physical resource, such as the trucks to create deliver the product to FedEx customers and create the signature FedEx experience. Novartis, the pharmaceutical giant, is highly dependent on its team of top scientists, as well as its highly qualified sales force to create and sell its medicines to doctors. Similarly, UBS Wealth Management is one of the premier banks in the world, but without its team of refined and knowledgeable bankers, UBS would fail to garner the same customer reviews and satisfaction as it does currently.

  1. Financial resources

The financial resource includes cash, lines of credit and the ability to have stock option plans for employees. All businesses have key resources in finance, but some will have stronger financial resources than other, such as banks that are based entirely on the availability of this key resource. Similarly, China Life insurance sells insurance to its wide customer base. However, if China Life Insurance does not have sufficient capital to cover insurance claims, it will not be able to survive in the market.

For a car manufacturer, the physical resources are in the facilities such as assembly robots. Another key resource could be intellectual property such as patents and even customer intelligence. The latter would come in very handy specifically knowing their preferences when you want to offer repeat customers special discounts and deals. For car manufacturers, designers would be a key human resource. In terms of financial resources, a manufacturer will require capital to invest in infrastructure and inventory but can additionally also be used to provide customers with the option of buying cars on lease or taking out a loan on better terms than those provided by banks or other financial institutions.

Key Partners(關鍵合作夥伴)

  • 誰是我們的關鍵合作夥伴?
  • 誰是我們的關鍵供應商?
  • 有哪些關鍵資源是從夥伴取得的?
  • 有哪些關鍵活動是夥伴執行的?

Optimization and economy of scale(分享)

Most organizations are heavily focused on the bottom-line. And many focus on cost-cutting or smart spending through optimizing the allocation of either their resources or activities. This is the most common motivation for people to enter into partnerships of different types.

When you are looking for efficiency in your company or optimizing your productions chains, key partners can help you achieve this goal. It is unrealistic to think, as an entrepreneur that you have the resources in place to conduct all your key activities in-house. Most partnerships give organizations the ability to share their infrastructures or outsource some activities to more cost-effective options.

Citroen, Peugeot and Toyota joined hands to create a small, cheap car for the masses that they tried to sell for 5000-6000 euros. These cars looked almost the same except for the chassis and a few internal and external details.

Reduction of risk and uncertainty(降低風險)

If you have a good relationship with a key partner, you reduce the inherent risk that comes with doing your own business. You also guarantee supply to your business rather than being dependent on suppliers who aren’t key partners and would therefore not give precedence to your business over others.

Many competitors may form strategic partnerships to share the risk of bringing something new into the market while still competing in various aspects in the industry. A classic example of this is the advent of blu-ray technology which was developed in collaboration by some of the world’s premier consumer electronics and computer technology firms. The development of this technology was expensive and several competitors had to get together and decide that they would all be selling their products based on blu-ray technology; hence they needed to collaborate to make blu-ray technology more mainstream. The group joined hands to bring the technology to the mass market but still competes on the basis of their various blu-ray based gadgets in the consumer market.

Acquisition of particular resources and activities(外包)

If there are certain things that you don’t have in-house and which would require a heavy investment of time, money or both, a key partner who already has these processes and the infrastructure developed would come in extremely handy.

Business models can be extensive maps of the myriad activities that a business needs to perform or the endless resources required to perform these activities successfully. However, it is rare for a new company to have the resources or capabilities in place to fulfill the mandate set down by the business model. Hence, many new companies are beginning their journeys by forming partnerships that give them access to the required resources or processes that they require, but are unable to own yet. Hence, many mobile operators partner with IT companies to develop the operating system their handsets require rather than bearing the heavy investment such an endeavor would require if done in-house. This also gives the IT company a steady source of revenue as well as the advantage of publicity if the mobile manufacturer’s brand is well recognized. Bicycle companies do not manufacture their bicycle accessories. Instead, they get into selective partnerships with bicycle parts manufacturers who customize the parts like the color or size of the bicycle seat according to the preferences of the manufacturer.

Heineken is one of the most popular producers and suppliers of beers in the world, and it is especially well-known in the Netherlands, where they have created very strong relationships with bar owners. In fact, Heineken frequently invests in new bars by providing not only equipment for free but also investing in the décor of the bar. In return, the bar provides Heineken beer exclusively. Hence, Heineken gets a repeat customer for their beer while the bar owner can minimize the cost of setting up the business. Conversely, however, the bar owner is limited to selling just Heineken, which means that if Heineken increases the prices of its beers, the bar owner has no choice but to abide by the new prices.

Cost Structure(成本結構)

  • 哪些是我們的商業模式中最重要的固有成本?
  • 哪個關鍵資源是最昂貴的?
  • 哪個關鍵活動是最昂貴的?

你的商業模式更偏重在->

  1. 成本驅動(輕瘦的成本結構, 低價格的價值主張, 盡可能的自動化, 大量的外包)
  2. 價值驅動(專注於創造價值, 額外費用的價值主張)

Fixed costs(固定成本(又稱固定費用)相對於變動成本,是指成本總額在一定時期和一定業務量範圍內,不受業務量增減變動影響而能保持不變的成本。)

固定成本

  • 約束性固定成本:
    為維持企業提供產品和服務的經營能力而必須開支的成本,如廠房和機器設備的折舊、財產稅、房屋租金、管理人員的工資等。由於這類成本與維持企業的經營能力相關聯,也稱為經營能力成本(capacity Cost)。這類成本的數額一經確定,不能輕易加以改變,因而具有相當程度的約束性。

  • 酌量性固定成本:
    企業管理當局在會計年度開始前,根據經營、財力等情況確定的計劃期間的預算額而形成的固定成本,如新產品開發費、廣告費、職工培訓費等。由於這類成本的預算數只在預算期內有效,企業領導可以根據具體情況的變化,確定不同預算期的預算數,所以,也稱為自定性固定成本。這類成本的數額不具有約束性,可以斟酌不同的情況加以確定。

Variable costs(變動成本是指那些成本的總發生額在相關範圍內隨著業務量的變動而呈線性變動的成本。直接人工、直接材料都是典型的變動成本,在一定期間內它們的發生總額隨著業務量的增減而成正比例變動,但單位產品的耗費則保持不變。)

可變成本

根據變動成本發生的原因可將變動成本分為兩類。一類是技術性變動成本、另一類是酌量性變動成本。

  • 技術性變動成本是指單位成本由技術因素決定而總成本隨著消耗量的變動而成正比例變動的成本,通常表現為產品的直接物耗成本。

  • 酌量性變動成本是指可由企業管理當局決策加以改變的變動成本。

Economies of scale( 大規模生產導致的經濟效益簡稱規模經濟(Economies of scale),是指在一定的產量範圍內,隨著產量的增加,平均成本不斷降低的事實。規模經濟是由於一定的產量範圍內,固定成本可以認為變化不大,那麼新增的產品就可以分擔更多的固定成本,從而使總成本下降。)

規模經濟

Economies of scope(當同時生產兩種產品的費用低於分別生產每種產品時,所存在的狀況就被稱為範圍經濟)

範圍經濟

Slack Hubot

簡單記錄一下,Slack Hubot 試驗過程:

(https://github.com/slackhq/hubot-slack)[安裝]

指令最好使用 sudo 以防有資料夾權限不夠操作的情況

(http://stackoverflow.com/questions/30657596/remove-heroku-setting-from-hubotmissing)[ HUBOT_HEROKU_KEEPALIVE_URL 問題排除]
碰到 ERROR hubot-heroku-alive included, but missing HUBOT_HEROKU_KEEPALIVE_URL. 問題請用上列方式處理。

接著專案的 scripts/example.coffee 有很多可玩的例子,例如:

robot.hear /badger/i, (res) ->
    res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"

只要對機器人說 "badger", 機器人便會自動回覆 "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"

比較進階一點的介紹:

(http://huli.logdown.com/posts/417258-hubot-a-bot-framework)[[心得] Hubot, 一套 bot framework]

可以把你的 Hubot 丟到Heruko,請參考此篇

(http://code.kpman.cc/2016/04/18/%E5%9C%A8-slack-%E5%BB%BA%E7%AB%8B-hubot/#六、將本地端 server 放上 heroku)[將本地端 server 放上 heroku]

其他Hubot可以串接的app

C也可以 TDD

參考說明載點

原文連結

C 其實也可以測試驅動開發的。

以下透過這個保齡球計分程式體驗 TDD in C 是怎樣的感覺(無需搭配其他框架)。

建立 bowling_game_test.c , 撰寫第一個測試 test_gutter_game()

bowling_game_test.c
#include <assert.h>
#include <stdbool.h>

static void test_gutter_game()
{
    assert(false && 'First test');
}

int main()
{
    test_gutter_game();
  
    return 0;
}

執行後測試失敗。

修改 test_gutter_game() 讓測試通過

bowling_game_test.c
#include <assert.h>
#include <stdbool.h>

static void test_gutter_game()
{
    bowling_game_init();

    for (int i = 0; i < 20; i ++) {
        bowling_game_roll(0);
    }
    
    //總分應該為0分
    assert(0 == bowling_game_score() && "test_gutter_game()");
}

int main()
{
    test_gutter_game();
}

執行測試
=> 錯誤訊息: bowling_game_init(), bowling_game_roll(), bowling_game_score() 都尚未定義,
=> 測試失敗

撰寫 bowling_game.h 和 bowling_game.c,實作上面三個函式

原本範例是把 bowling_game.h 和 bowling_game.c 拆成兩動,這邊我是覺得應該以"足以解決一個測試" 的動作當一動比較適當,因此只做將兩個步驟合成一個。

bowling_game.h
void bowling_game_init();
void bowling_game_roll(int pins);
int bowling_game_score();

bowling_game.c
#include "bowling_game.h"

static int score;

void bowling_game_init() {
}

void bowling_game_roll(int pins) {
}

int bowling_game_score() {
    return -1;
}

執行測試 => 錯誤訊息

assert(0 == bowling_game_score() && "test_gutter_game()");

=> 測試失敗

改寫程式碼讓 (0 == bowling_game_score() && "test_gutter_game()")

bowling_game.c
#include "bowling_game.h"

static int score;

void bowling_game_init() {
    score = 0;
}

void bowling_game_roll(int pins) {
}

int bowling_game_score() {
    return score;
}

執行測試 => 沒有發生錯誤 => 測試通過。

撰寫新的測試 test_all_ones()

scenario:

每一次丟球都擊倒一個球瓶。

bowling_game_test.c
// ....

static void test_all_ones() {
    bowling_game_init();

    for (int i = 0; i < 20; i ++) {
        bowling_game_roll(1);
    }

    //總分應該為20分
    assert(20 == bowling_game_score() && "test_all_ones()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    
    return 0;
}

執行測試
=> 錯誤訊息

Assertion failed: (20 == bowling_game_score() && "test_all_ones()")

=> 測試失敗

修改 bowling_game_roll() 讓測試通過

bowling_game_test.c
// ...

void bowling_game_roll(int pins) {
    score += pins;
}

// ...

執行測試 => 沒有出現錯誤 => 測試通過

重複的程式碼包裝成函式

test_gutter_game() 和 test_all_ones() 執行的 for 迴圈可包裝成函式共用,程式碼重構後如下:

bowling_game_test.c
// ...

static void roll_many(int n, int pins) {
    for (int i = 0; i < n; i ++) {
        bowling_game_roll(pins);
    }
}

static void test_gutter_game() {
    bowling_game_init();

    roll_many(20, 0);

    //總分應該為0分
    assert(0 == bowling_game_score() && "test_gutter_game()");
}

static void test_all_ones() {
    bowling_game_init();

    roll_many(20, 1);

    // 總分應該為20分
    assert(20 == bowling_game_score() && "test_all_ones()");
}

// ...

執行測試 => 沒有錯誤訊息 => 測試通過

撰寫新的測試 test_one_spare()

bowling_game_test.c
// ...

static void test_one_spare() {
    bowling_game_init();

    // 先投三次
    bowling_game_roll(5);
    bowling_game_roll(5); // 得到一個 spare
    bowling_game_roll(3);

    // 餘下的 17 次假設擊倒球瓶全部為 0 
    roll_many(17, 0);

    assert(16 == bowling_game_score() && "test_one_spare()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();

    return 0;
}

執行測試
=> 錯誤訊息

Assertion failed: (16 == bowling_game_score() && "test_one_spare()")

=> 測試失敗

檢視一下程式碼,可以發現兩個問題:

1. bowling_game_roll() 有計算分數,但函式名稱看不出來有他這個功用
2. bowling_game_score() 沒有計算的動作,但函式名稱會讓人誤以為有計算的動作

=> 這樣的設計是有問題的,各函式該負責的事情搞錯了。

重構 bowling_game.c

bowling_game.c
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {MAX_ROLLS = 21};
static int rolls[MAX_ROLLS];
static int current_roll;

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;

    for (int i = 0; i < MAX_ROLLS; i ++) {
        score += rolls[i];
    }

    return score;
}

執行測試
=> 錯誤訊息

Assertion failed: (16 == bowling_game_score() && "test_one_spare()")

=> 測試未通過

bowling_game_score() 的 spare 功能並未實作出來,繼續改寫。

改寫 bowling_game_score() 以通過 test_one_spare()

bowling_game_test.c
// ...

// static void test_one_spare() {
//     bowling_game_init();

//     // 先投三次
//     bowling_game_roll(5);
//     bowling_game_roll(5); // 得到一個 spare
//     bowling_game_roll(3);

//     // 餘下的 17 次假設擊倒球瓶全部為 0 
//     roll_many(17, 0);

//     assert(16 == bowling_game_score() && "test_one_spare()");
// }

int main() {
    test_gutter_game();
    test_all_ones();
    //test_one_spare();

    return 0;
}

註解掉test_one_spare() 是因為我們要重新設計 bowling_game_score(),透過原本已經 passed 的 test_gutter_game() 和 test_all_ones() 檢驗改寫的 bowling_game_score() 是否正確。

bowling_game.c
//...

int bowling_game_score() {
    int score = 0;
    int i = 0;

    for (int frame = 0; frame < 10; ++ frame) {
        score += rolls[i] + rolls[i + 1];
        i += 2;
    }

    return score;
}

//...

執行測試 => 沒有錯誤訊息 => 測試通過

表示 bowling_game_score() 重構後是正確的,因此我們可以繼續剛剛註解掉的 test_one_spare()。

取消對 test_one_spare() 的註解

bowling_game_test.c
// ...

static void test_one_spare() {
    bowling_game_init();

    // 先投三次
    bowling_game_roll(5);
    bowling_game_roll(5); // 得到一個 spare
    bowling_game_roll(3);

    // 餘下的 17 次假設擊倒球瓶全部為 0 
    roll_many(17, 0);

    assert(16 == bowling_game_score() && "test_one_spare()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();

    return 0;
}

執行測試
=> 出現錯誤訊息

Assertion failed: (16 == bowling_game_score() && "test_one_spare()")

繼續針對 bowling_game_score() 改寫 以通過 test_one_spare()

bowling_game.c
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};
static int rolls[MAX_ROLLS];
static int current_roll;

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;
    int i = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (MAX_SCORE_PER_FRAME == rolls[i] + rolls[i + 1]) {
            score += MAX_SCORE_PER_FRAME + rolls[i + 2];
            i += 2;
        } else {
            score += rolls[i] + rolls[i + 1];
            i += 2;
        }
    }

    return score;
}

執行測試 => 沒有錯誤訊息 => 測試通過

修改bowling_game.c 增加程式碼可讀性

bowling_game.c
#include <stdbool.h>
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};
static int rolls[MAX_ROLLS];
static int current_roll;

static bool is_square(int roll_index) {
    return (10 == rolls[roll_index] + rolls[roll_index + 1]);
}

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (is_square(roll_index)) {
            score += MAX_SCORE_PER_FRAME + rolls[roll_index + 2];
            roll_index += 2;
        } else {
            score += rolls[roll_index] + rolls[roll_index + 1];
            roll_index += 2;
        }
    }

    return score;
}

執行測試 => 沒有錯誤訊息 => 測試通過

撰寫新的測試 test_one_strike()

bowling_game_test.c
// ...

static void test_one_strike() {
    bowling_game_init();

    bowling_game_roll(10);
    bowling_game_roll(3);
    bowling_game_roll(4);

    roll_many(17, 0);

    assert(24 == bowling_game_score() && "test_one_strike");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();
    test_one_strike();

    return 0;
}

執行測試
=> 發生錯誤訊息

Assertion failed: (24 == bowling_game_score() && "test_one_strike")

=> 測試失敗

修改 bowling_game.c 以通過測試 test_one_strike()

bowling_game.c
// ...

static bool is_strike(int roll_index) {
    return 10 == rolls[roll_index];
}

// ...

int bowling_game_score() {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (is_strike(roll_index)) {
            score += 10 + rolls[roll_index + 1] + rolls[roll_index + 2];

            roll_index += 1; 
        } else if (is_square(roll_index)) {
            score += MAX_SCORE_PER_FRAME + rolls[roll_index + 2];

            roll_index += 2;
        } else {
            score += rolls[roll_index] + rolls[roll_index + 1];

            roll_index += 2;
        }
    }

    return score;
}

執行測試 => 沒有錯誤訊息 => 測試通過

修改 bowling_game.c 增加程式碼可讀性

bowling_game.c
#include <stdbool.h>
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};
static int rolls[MAX_ROLLS];
static int current_roll;

static bool is_square(int roll_index) {
    return (MAX_SCORE_PER_FRAME == rolls[roll_index] + rolls[roll_index + 1]);
}

static bool is_strike(int roll_index) {
    return MAX_SCORE_PER_FRAME == rolls[roll_index];
}

static int strike_score(int roll_index) {
    return MAX_SCORE_PER_FRAME + rolls[roll_index + 1] + rolls[roll_index + 2];
}

static int spare_score(int roll_index) {
    return MAX_SCORE_PER_FRAME + rolls[roll_index + 2];
}

static int normal_score(int roll_index) {
    return rolls[roll_index] + rolls[roll_index + 1];
}

void bowling_game_init() {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        rolls[i] = 0;
    }

    current_roll = 0;
}

void bowling_game_roll(int pins) {
    rolls[current_roll] = pins;

    current_roll ++;
}

int bowling_game_score() {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; ++ frame) {
        if (is_strike(roll_index)) {
            score += strike_score(roll_index);

            roll_index += 1; 
        } else if (is_square(roll_index)) {
            score += spare_score(roll_index);

            roll_index += 2;
        } else {
            score += normal_score(roll_index);

            roll_index += 2;
        }
    }

    return score;
}

撰寫新的測試 test_perfect_game()

bowling_game_test.c
// ...

static void test_perfect_game() {
    bowling_game_init();

    roll_many(12, 10);

    assert(300 == bowling_game_score() && "test_perfect_game()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();
    test_one_strike();
    test_perfect_game();

    return 0;
}


執行測試 => 沒有錯誤訊息 => 測試通過

小結

到這邊程式的撰寫就算完成了,但用了 rolls 和 current_roll 兩個全域變數不是很好,可以再進一步將這兩個變數包進 struct

再次重構

bowling_game_test.c
#include "bowling_game.h"

#include <assert.h>
#include <stdbool.h>

static void roll_many(struct bowling_game * game, int n, int pins) {
    for (int i = 0; i < n; i ++) {
        bowling_game_roll(game, pins);
    }
}

static void test_gutter_game() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    roll_many(game, 20, 0);

    //總分應該為0分
    assert(0 == bowling_game_score(game) && "test_gutter_game()");
}

static void test_all_ones() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    roll_many(game, 20, 1);

    // 總分應該為20分
    assert(20 == bowling_game_score(game) && "test_all_ones()");
}

static void test_one_spare() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    // 先投三次
    bowling_game_roll(game, 5);
    bowling_game_roll(game, 5); // 得到一個 spare
    bowling_game_roll(game, 3);

    // 餘下的 17 次假設擊倒球瓶全部為 0 
    roll_many(game, 17, 0);

    assert(16 == bowling_game_score(game) && "test_one_spare()");
}

static void test_one_strike() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    bowling_game_roll(game, 10);
    bowling_game_roll(game, 3);
    bowling_game_roll(game, 4);

    roll_many(game, 17, 0);

    assert(24 == bowling_game_score(game) && "test_one_strike()");
}

static void test_perfect_game() {
    struct bowling_game * game = bowling_game_create();

    bowling_game_init(game);

    roll_many(game, 12, 10);

    assert(300 == bowling_game_score(game) && "test_perfect_game()");
}

int main() {
    test_gutter_game();
    test_all_ones();
    test_one_spare();
    test_one_strike();
    test_perfect_game();

    return 0;
}


bowling_game.h
struct bowling_game;
struct bowling_game * bowling_game_create();

void bowling_game_init(struct bowling_game * game);
void bowling_game_roll(struct bowling_game * game, int pins);
int bowling_game_score(struct bowling_game * game);

bowling_game.c
#include <stdbool.h>
#include <stdlib.h>
#include "bowling_game.h"

// enum 用法參考 http://bodscar.pixnet.net/blog/post/61204511-%E8%AA%AA%E6%98%8E-typedef-enum
// 不過注意一下最後的範例是錯的
enum {
    MAX_ROLLS = 21,
    FRAME_LENGTH = 10,
    MAX_SCORE_PER_FRAME = 10
};

struct bowling_game {
    int rolls[MAX_ROLLS];
    int current_roll;
};

struct bowling_game * bowling_game_create() {
    struct bowling_game * game = malloc(sizeof(struct bowling_game));

    for (int i = 0; i < MAX_ROLLS; i ++) {
        game->rolls[i] = 0;
    }

    game->current_roll = 0;

    return game;
}

static bool is_square(struct bowling_game * game, int roll_index) {
    return (MAX_SCORE_PER_FRAME == game->rolls[roll_index] + game->rolls[roll_index + 1]);
}

static bool is_strike(struct bowling_game * game, int roll_index) {
    return MAX_SCORE_PER_FRAME == game->rolls[roll_index];
}

static int strike_score(struct bowling_game * game, int roll_index) {
    return MAX_SCORE_PER_FRAME + game->rolls[roll_index + 1] + game->rolls[roll_index + 2];
}

static int spare_score(struct bowling_game * game, int roll_index) {
    return MAX_SCORE_PER_FRAME + game->rolls[roll_index + 2];
}

static int normal_score(struct bowling_game * game, int roll_index) {
    return game->rolls[roll_index] + game->rolls[roll_index + 1];
}

void bowling_game_destroy(struct bowling_game * game) {
    free(game);

    game = NULL;
}

void bowling_game_init(struct bowling_game * game) {
    for (int i = 0; i < MAX_ROLLS; i ++) {
        game->rolls[i] = 0;
    }

    game->current_roll = 0;
}

void bowling_game_roll(struct bowling_game * game, int pins) {
    game->rolls[game->current_roll] = pins;

    game->current_roll ++;
}

int bowling_game_score(struct bowling_game * game) {
    int score = 0;
    int roll_index = 0;

    for (int frame = 0; frame < FRAME_LENGTH; frame ++) {
        if (is_strike(game, roll_index)) {
            score += strike_score(game, roll_index);

            roll_index += 1; 
        } else if (is_square(game, roll_index)) {
            score += spare_score(game, roll_index);

            roll_index += 2;
        } else {
            score += normal_score(game, roll_index);

            roll_index += 2;
        }
    }

    return score;
}

感謝您的耐心閱讀。

程式碼打包下載

IE6 不支援 getElementsByClassName 解決辦法

FB 上看到朋友分享的順手轉貼。( 話說現在還用IE6 的應該很少了吧 )
所以要自己定義該方法,程式碼如下:

fixIE6getElementsByClassName.js
if (!document.getElementsByClassName) { // 若不存在 getElementsByClassName 方法則進入控制流

  document.getElementsByClassName = function (className, element) {
    var children = (element || document).getElementsByTagName('*');
    var elements = new Array();
    
    for (var i = 0; i < children.length; i ++) { // 逐一檢視抓取的元素

      var child = children[i];
      var classNames = child.className.split(' '); // class間會以空白間隔,故根據空白切割產生陣列

      
      for (var j = 0; j < classNames.length; j ++) { // 逐一比對 class 名稱

        if (classNames[j] === className) { // 若 class 名稱相符,將其置入回傳之陣列中,並且跳脫此迴圈

            elements.push(child);
            break;
        }
      }
    } 

    return elements; // 回傳取得的元素

  };
}

Laravel-Eloquent ORM(下)

Eager Loading

關於什們是 Eager loading 的簡單介紹:

Eager loading: 把所有的事情在詢問時就做完。典型的例子是,當您將兩個矩陣增加時,就把所有的運作都做完了。
Lazy loading: 只有在需要的時候才去進行運算。以上述例子來說,您在取得矩陣內某個元素時才會去進行運算。
Over-eager loading: 預測使用者將會詢問什們然後預先將起加載完畢。

Eager Loading 是為了解決 N+1 query(關於 N+1 query請參考此文)的問題而存在的。舉例來說,一本書(Book)和一個作者(Author)關連。它們的關聯定義如下:

<?php

class Book extends Eloquent {

    public function author()
    {
        return $this->belongsTo('Author');
    }

}

請思考以下程式碼:

<?php

foreach (Book::all() as $book)
{
    echo $book->author->name;
}

這個迴圈會先執行一個取得所有書本的 query,然後再根據每一本書去取得作者。因此,如果我們有 25 本書,那們將會執行 26 次的 query。

幸好,我們可以使用 Eager loading 去大幅度的減少 query 的數量。我們可以藉由 with()方法去指定此關連應該使用 eager loaded:

<?php

foreach (Book::with('author')->get() as $book)
{
    echo $book->author->name;
}

在上面的迴圈中,只有兩條 query 會被執行:

<?php

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

適當的使用 Eager Loading 可以大大的增加應用程式的效能。當然,您也可以一次對多個關連做 eager load:

<?php

$books = Book::with('author', 'publisher')->get();

也可以對巢狀關連做 Eager Load:

<?php

$books = Book::with('author.contacts')->get();

在上面的例子中,author 的將會被 eager load,而 author 的 contacts關連也會被 eager load。

Eager Load 限制

有些時候您可能會想要在 Eager load 上做一點限制。如下:

<?php

$users = User::with(array('posts' => function($query)
{
    $query->where('title', 'like', '%first%');

}))->get();

當然,您也可以加上排序:

<?php

$users = User::with(array('posts' => function($query)
{
    $query->orderBy('created_at', 'desc')

}))->get();

Lazy Eager Loading

我們也可以從已經存在的model 集合中去 eager load相關連的 model。當我們需要動態的決定使否讀取關連或是和 cache 結合使用的時候,這會很有用:

<?php

$books = Book::all();

$books->load('author', 'publisher');

Inserting Related Models

您可能也會常常需要去新增一個關連 model。舉例來說,您想要新增一個新的 comment,這個comment 屬於某個 post。您可以直接從 Post model 直接新增一筆 comment取代手動的去設置 comment 的外鍵。

增加一個關連 model
<?php

$comment = new Comment(array('message' => 'A new comment.'));

$post = Post::find(1);

$comment = $post->comments()->save($comment);

在上面的例子中,新增的comment記錄中的 post_id 欄位被自動被賦值。

Associating Models (Belongs To)

您可以使用 associate()方法去更新 belongsTo關連,這個方法會對子model的外鍵賦值。

<?php

$account = Account::find(10);

$user->account()->associate($account);

$user->save();

新增關連模組 (多對多)

同樣的我們可以在多對多的關聯中新增關連model。延續前面的 User 和 Role model 當做我們的例子。我們可以透過 attach()方法很簡單的給予某個 user 新的 role。

附上多對多 model
<?php

$user = User::find(1);

$user->roles()->attach(1);

您也可以傳送一個屬性陣列去修改關連 model 的值:

<?php

$user->roles()->attach(1, array('expires' => $expires));

當然,有 attach()方法就有detach()方法:

<?php

$user->roles()->detach(1);

您也可以使用 sync()方法去附加關連 model。sync()方法會接受一個 ID 的陣列,並將它存放在樞紐表格。在完成這個操作後,只有陣列中的ID 會出現在中介表格。

使用同步方法去附加多對多 model
<?php

$user->roles()->sync(array(1, 2, 3));

您也可以藉由給定的 ID 陣列改變樞紐表格的值

同步時更改樞紐表單資料
<?php

$user->roles()->sync(array(1 => array('expires' => true)));

有時候您可能會希望新增一個關連model並且將它附加上去。這時可以使用 save()方法:

<?php

$role = new Role(array('name' => 'Editor'));

User::find(1)->roles()->save($role);

您也可以傳入一個屬性陣列修改關連的表格資料。

<?php

User::find(1)->roles()->save($role, array('expires' => $expires));

Touching Parent Timestamps

當一個 model 屬於另一個 model 時,例如 comment 屬於一個 Post,在子model被呼叫時同時更新父 model 的 timestamp會很有幫助。舉例來說,當一個 Comment model被更新時,您可能會想要自動的去 touch 擁有該 comment 的 Post 的 updated_at timestamp。Eloquent 使得這件事情很容易。只要加一個 touches 屬性,該屬性為一個包含關連model 名稱的陣列。

<?php

class Comment extends Eloquent {

    protected $touches = array('post');

    public function post()
    {
        return $this->belongsTo('Post');
    }

}

現在,當您更新一個 Comment,擁有它的Post的 updated_at 欄位也會同步被更新。

<?php

$comment = Comment::find(1);

$comment->text = 'Edit to this comment!';

$comment->save();

Working With Pivot Tables

如您所知道的,處理多對多關連需要一個中介表格。Eloquent 提供了一些非常方便的方法和這些表格互動。繼續沿用 User 和Role 的例子。在取得關連後,我們可以從 model 中取得樞紐表格:

<?php

$user = User::find(1);

foreach ($user->roles as $role)
{
    echo $role->pivot->created_at;
}

每個我們取得的 Role model 都會自動被賦與一個 pivot 屬性。該屬性包含了一個代表中介表格的 model,而您也可以像使用其他 Eloquent model 般的使用它。預設只有鍵會存在樞紐物件上,如果您的樞紐表格包含了其他屬性,必須在定義關連時指定它們。

<?php

return $this->belongsToMany('Role')->withPivot('foo', 'bar');

現在 foo 和 bar 屬性可以從樞紐物件取得了。如果您想要樞紐物件自動的維護 created_at 和 updated_at 這些 timestamps,請在關連定義的地方使用withTimestamps()方法。

<?php

return $this->belongsToMany('Role')->withTimestamps();

將某個 model 的樞紐表格記錄全部刪除,可以使用 detach 方法:

刪除樞紐表格的紀錄
<?php

User::find(1)->roles()->detach();

這的動作並不會刪除 roles 表格的紀錄,僅只會刪除樞紐表格的紀錄。

定義一個客製樞紐model

Laravel 同樣允許您定義一個客製樞紐model。為了定義一個客製 model,首先您必須新增您的 "基底" model 類別,該類別需為 Eloquent類別的延伸。在您的其他 Eloquent model 中,將延伸的類別由預設的 Eloquent 改為您客製化的 base model。在您的base model中,加入下述函式以回傳一個您的客製樞紐實體。

<?php

public function newPivot(Model $parent, array $attributes, $table, $exists)
{
    return new YourCustomPivot($parent, $attributes, $table, $exists);
}

Collection

所以Eloquent取得的超過一個以上的結果,都會回傳一個 Collention 物件。這個物件實作了 PHP的 IteratorAggregate 介面,所以可以像陣列般的迭代訪問。當然,這個物件也有許多方便我們操作的方法。

舉例來說,我們可以使用 contains()方法判斷是否有包含我們指定的主鍵:

檢查 Collection 是否包含某個鍵
<?php

$roles = User::find(1)->roles;

if ($roles->contains(2))
{
    //
}

collection 可能會被轉換為陣列或是JSON。

<?php

$roles = User::find(1)->roles->toArray();

$roles = User::find(1)->roles->toJson();

如果 collection 為字串,會被自動轉換成JSON格式。

<?php

$roles = (string) User::find(1)->roles;

Eloquent 的 collection 也有一些不錯的方法可以迭代或是過濾包含的物件。

迭代 collection
<?php

$roles = $user->roles->each(function($role)
{
    //
});

過濾 collection

當使用 filter()方法時,它的 callback 函式作用相當於php 的 array_filter:

<?php

$users = $users->filter(function($user)
{
    return $user->isAdmin();
});

注意: 當過濾一個collection 並且將它轉換為 JSON時,請先呼叫 values() 函式重新設置陣列的鍵值。可參考此文

對每個 collection 物件加上 callback
<?php

$roles = User::find(1)->roles;

$roles->each(function($role)
{
    //
});
根據某個值排序 collection
<?php

$roles = $roles->sortBy(function($role)
{
    return $role->created_at;
});

也可以這樣:

<?php

$roles = $roles->sortBy('created_at');

有時候您會希望自訂的方法回傳客製的 collection 物件,這可以藉由覆寫您的 Eloquent model 中的方法來實現:

回傳一個客製的 Collection
<?php

class User extends Eloquent {

    public function newCollection(array $models = array())
    {
        return new CustomCollection($models);
    }

}

Accessors & Mutators

Laravel 提供了一個方便的方法在取得或設定您的model屬性時轉換它們。在您的 model 簡單定義一個 getFooAttribute 方法去宣告一個 accessor( 訪問器 )。注意方法的名稱必須採大小寫駝峰式,就算您的資料庫欄位名稱是小寫底線式。

定義一個取得器
<?php

class User extends Eloquent {

    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }

}

在上面的例子中,first_name 欄位有了一個取得器,屬性的值會被傳入取得器中。

定義一個設置器
<?php

class User extends Eloquent {

    public function setFirstNameAttribute($value)
    {
        $this->attributes['first_name'] = strtolower($value);
    }

}

Date Mutators

Eloquent 預設會把 created_at、updated_at、以及 deleted_at 的欄位轉換成 Carbon 的實體,Carbon 類別是原生 PHPDateTime 類別的延伸,提供了許多實用的方法。

藉由覆寫 getDates()方法,您可以自行指定哪些欄位需要進行轉換,或是所有欄位都取消轉換。

<?php

public function getDates()
{
    return array('created_at');
}

當一個欄位被認為是日期時,您可以將它的值設定為 UNIX timestamp,日期字串( Y-m-d ),日期+時間字串( Y-m-d H:i:s),當然,也可以是一個 Datetime/Carbon 實體。

如果要完全取消轉換,將 getDates()方法改寫如下即可:

<?php

public function getDates()
{
    return array();
}

Model Events

Eloquent model 有一些事件驅動的方法,creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored,讓您可以在 model 的生命週期中自行添加一些處理。

每當一個新的物件第一次被儲存時, creating 和 created 事件會被觸發。如果一個舊有物件被儲存,那個updating/update 方法會被觸發。以上兩個例子中, saving 和 save 事件都會被觸發。

如果從 creating, updating, saving, deleting 事件回傳了 false,那該動作會被取消。

藉由事件取消一個儲存操作
<?php

User::creating(function($user)
{
    if ( ! $user->isValid()) return false;
});

Eloquent model 同時包含了一個靜態方法 boot(),boot() 方法提供了一個方便的空間讓您註冊事件綁定。

設置一個 model 的 Boot 方法
<?php

class User extends Eloquent {

    public static function boot()
    {
        parent::boot();

        // Setup event bindings...
    }

}

Model Observers

為了統一管理 model 的事件,您可以註冊一個 model觀察者( model observer )。一個 Observer 類別會有許多方法對應不同的 model 事件。舉例來說, creating,updating,saving 方法存在某個 observer 中,或是其他 model 中的事件名稱。

如下,一個 model observer 可能如下:

<?php

class UserObserver {

    public function saving($model)
    {
        //
    }

    public function saved($model)
    {
        //
    }

}

您也可以註冊一個 observer實體來使用 observer方法:

<?php

User::observe(new UserObserver);

Converting To Arrays / JSON

當建立 JSON API時,您可能常常需要將您的 model 或是關連model 轉換成陣列或是JSON。將 model 或是其關連轉換成陣列,您可以使用 toArray()方法:

將一個 model 轉換成陣列
<?php

$user = User::with('roles')->first();

return $user->toArray();

整個 model 的 collection 也可以被轉換為陣列

<?php

return User::all()->toArray();

將 model 轉換成 JSON,可以使用 toJson()方法:

將 model 轉換成 JSON
<?php

return User::find(1)->toJson();

如果 model 或是 collection 被預測成一個字串,它最後將會被轉成JSON輸出。這意味著您可以直接從 route 回傳 Eloquent 物件。

從 Route 回傳一個 Model
<?php

Route::get('users', function()
{
    return User::all();
});

有時候您可會希望限制某些屬性包含在 model 陣列或是 JSON 格式中,例如密碼這種敏感的資料。可以透過在 model 中增加 hidden 屬性來實現:

<?php

class User extends Eloquent {

    protected $hidden = array('password');

}

注意: 如果隱藏的是關連model 的屬性,要使用關連的方法名稱,而不是(Dynamic accessor)動態取得器的名稱。( 其實兩者大部分情況下應該都一樣吧? )

當然,您也可以使用 visible 屬性定義一份白名單:

<?php

protected $visible = array('first_name', 'last_name');

有時候您可能會需要加入一個和資料庫欄位名稱不一致的屬性。

<?php

public function getIsAdminAttribute()
{
    return $this->attributes['admin'] == 'yes';
}

一旦您建立了 accessor,在 model 中將該值加入append 屬性裡。

<?php

protected $appends = array('is_admin');

一旦該屬性被加入 appends 表單中,model array 和 JSON 都將會包含該屬性。

本篇回顧

非常非常多內容的一篇,終於翻完了好感動。( 好像還有三篇 orz )

許多內容我並沒有實際去手動實驗驗證,可能有些地方我的理解是錯的,不過就先暫時這樣了,之後開始實作再回來改正。Eloquent 沒想到東西這們多,看的好累...,不過和Doctrine比還是好多了。

參考資料

Laravel Eloquent ORM: http://laravel.com/docs/eloquent#eager-loading

Laravel-Eloquent ORM(上)

Introduction

Eloquent ORM 為您的database操作提供了一個優雅且簡單的 ActiveRecord 的實作。每個 database 的 table 有一個對應的 "Model",這個 Model 會被用來和 table 互動。

在我們開始之前,請確認在 app/config/database.php 裡有先設定好一個資料庫的連結。

Basic Usage

首先,新增一個 Eloquent Model。Models 一般來說存放於 app/models 的目錄中,但和 controller一樣,您可以將它存放於任何您希望的位置,只要確保檔案能夠根據 composer.json 定義的路徑自動讀取到它們。

定義一個Eloquent Model

<?php

class User extends Eloquent {}

注意,我們目前還沒告訴 Eloquent User 是和哪個 table 對應。如果沒有特別指定的話,小寫複數型態的 Model 類別名稱會被當做對應的 table 名稱。所以在這個例子中,User Model 對應的是 users table。您可以藉由定義一個table屬性自行指定一個對應的 table 名稱:

<?php

class User extends Eloquent {

    protected $table = 'my_users';

}

注意: Eloquent 會假設每個 table 都有一個欄位名稱為 id 的主鍵,您可以定義一個 primaryKey 屬性去覆寫這個轉換規則。除此之外,您也可以定義一個 connection 屬性去覆寫使用 model 時的資料庫連結名稱。

取得所有的 Models
<?php

$users = User::all();
根據主鍵取得一筆記錄
<?php

$user = User::find(1);

var_dump($user->name);

注意: 所有在 Query Builder 可以使用的方法,在Eloquent models 也可以使用!!

藉由主鍵取得一個 Model 或是丟出一個 exception

有時候您可能會希望在找不到 model 時丟出一個 exception,可以使用如下方法:

<?php

$model = User::findOrFail(1);

$model = User::where('votes', '>', 100)->firstOrFail();

註冊一個 error handler,並且監聽 ModelNotFoundException

<?php

use Illuminate\Database\Eloquent\ModelNotFoundException;

App::error(function(ModelNotFoundException $e)
{
    return Response::make('Not Found', 404);
});

使用 Eloquent 執行 Query
<?php

$users = User::where('votes', '>', 100)->take(10)->get();

foreach ($users as $user)
{
    var_dump($user->name);
}

當然,您也可以使用Query Builder 的統計函式。

Eloquent 統計
<?php

$count = User::where('votes', '>', 100)->count();

如果您無法藉由該介面產生您要的 query,可以使用 whereRaw()方法:

<?php

$users = User::whereRaw('age > ? and votes = 100', array(25))->get();

將結果切塊

如果您需要處理非常大量的 Eloquent 記錄,使用 chunk()方法將結果將成許多小塊可以避免您的暫存記憶體被耗盡:

<?php

User::chunk(200, function($users)
{
    foreach ($users as $user)
    {
        //
    }
});

第一個參數是設定每個切塊包含的記錄數量,而第二個閉包參數則於每次從資料庫抓取切塊時呼叫。

指定 Query 使用的連結

您可以在執行 Eloquent Query 時指定要使用哪個資料庫連結:

<?php

$user = User::on('connection-name')->find(1);

Mass Assignment

當新增一個 model 時,我們會傳遞一個屬性的陣列給 model 的建構子,而該陣列將會對 model內對應的屬性賦值。如下:

<?php

$user = new User;
$user->fill(array('name'=>'Jordan', 'email'=>'jordan@hotmail.com'));
$user->save();

這相當方便。然而,如果盲目的將使用者提供的數值送入 model,讓使用者可以任意修改 model 的屬性,可能會造成嚴重的安全問題,因此所有的 Eloquent Model 預設都會防止 Mass Assignment。

fillable屬性可以指定哪些屬性是Mass-assignable,可以在類別或是實體設定。

在 Model 定義一個 Fillable 屬性
<?php

class User extends Eloquent {

    protected $fillable = array('first_name', 'last_name', 'email');

}

在這個例子中,只有陣列中的三個屬性是Mass-assignable。fillable 的相反是 guarded。

在 Model 定義一個 Guarded 屬性
<?php

class User extends Eloquent {

    protected $guarded = array('id', 'password');

}

在上面的例子中, id 和 password 無法被 mass assignment。其他的屬性則接可以被mass assignment。您也可以直接使用guard屬性限制任何屬性都不可被mass assignment。

<?php

protected $guarded = array('*');

Insert, Update, Delete

透過新增一個 Model 實體並且呼叫 save()方法,從Model 中新增一筆記錄進入資料庫:

<?php

$user = new User;

$user->name = 'John';

$user->save();

注意: 一般來說,Eloquent Model 會有一個自動累加的鍵,然而,如果您想要指定一個自行定義的鍵,可以把 incrementing 屬性設置為 false。

您也可以使用 create() 方法儲存一個新增的 model。該方法最後會回傳新增的 Model 之實體。然而,在這們做之前,您需要指定 fillable 或是 guarded 屬性,否則 Eloquent 會自動防止所有的 mass-assignment。

在使用自動累加ID 新增或儲存完一個 Model 後,您可以藉由 id 屬性取得 ID。

<?php

$insertedId = $user->id;
在 Model 上設定 Guarded屬性
<?php

class User extends Eloquent {

    protected $guarded = array('id', 'account_id');

}
使用 Model 的 create() 方法
<?php

// Create a new user in the database...
$user = User::create(array('name' => 'John'));

// Retrieve the user by the attributes, or create it if it doesn't exist...
$user = User::firstOrCreate(array('name' => 'John'));

// Retrieve the user by the attributes, or instantiate a new instance...
$user = User::firstOrNew(array('name' => 'John'));

要更新一個 Model,您可以先取得它,在重新設置它的屬性,最後儲存。

更新一個取得的 Model
<?php

$user = User::find(1);

$user->email = 'john@foo.com';

$user->save();

有時候我們不只想要儲存一個 Model,同時也想要將其關連的 model 也一起儲存。這是可以使用 push()方法:

<?php

$user->push();

您也可以一次對某範圍的 models 執行 update():

<?php

$affectedRows = User::where('votes', '>', 100)->update(array('status' => 2));

刪除一個已經存在的 Model
<?php

$user = User::find(1);

$user->delete();

根據鍵值刪除一個已經存在的 Model
<?php

User::destroy(1);

User::destroy(array(1, 2, 3));

User::destroy(1, 2, 3);

當然,也可以一次刪除多個 Model

<?php

$affectedRows = User::where('votes', '>', 100)->delete();

如果您僅只是想更新 model 上的 timestamp,可以使用 touch()方法:

<?php

$user->touch();

Soft Deleting

當您軟刪除( sofe delete )一個 model時,其實並沒有真正的將資料從database刪除。取而代之的是,一個 deleted_at 的timestamp 會被設置在該筆記錄上。設置 softDelete屬性讓 model 允許使用軟刪除。

<?php

class User extends Eloquent {

    protected $softDelete = true;

}

要在表格增加一個 deleted_at欄位,您可以在 migration 使用 softDeletes()方法:

<?php

$table->softDeletes();

之後,當您在 model 上使用 delete()方法時,deleted_at欄位得值會被設置為現在的時間。

被軟刪除的 model 不會包含在之後query 的結果中,必需使用 withTrashed()方法取得:

強制被軟刪除的 model 可以在 query 的結果中取得
<?php

$users = User::withTrashed()->where('account_id', 1)->get();

withTrashed() 方法也可以用在已經定義的關聯 model 上:

<?php

$user->posts()->withTrashed()->get();

如果您只想要取得被軟刪除的 model,可以使用 onlyTrashed()方法:

<?php

$users = User::onlyTrashed()->where('account_id', 1)->get();

使用 restore()方法將被軟刪除的 model 復原。

<?php

$user->restore();

也可以將 restore()方法在 query 上使用:

<?php

User::withTrashed()->where('account_id', 1)->restore();

和 withTrashed()方法一樣, restore()方法也可以用在關連 model 上:

<?php

$user->posts()->restore();

如果您想要將 model 真的從 database移除,可以使用 forceDelete()方法。

<?php

$user->forceDelete();

forceDelete()方法也可以使用在關連的 model 上:

<?php

$user->posts()->forceDelete();

判斷一個取得的實體是否已經被軟刪除,可以使用 trashed()方法:

<?php

if ($user->trashed())
{
    //
}

Timestamps

只要您有加上created_at 和 updated_at 這些 timestamp 欄位, Eloquent 預設會自動幫我們維護它們。如果您不希望 Eloquent 自動幫我們維護這些欄位,可以在 model 加入以下屬性:

取消自動 Timestamps
<?php

class User extends Eloquent {

    protected $table = 'users';

    public $timestamps = false;

}

如果您希望客製化您的 timestamp 格式,可以在您的 model 裡覆寫 getDateFormat()方法。

<?php

class User extends Eloquent {

    protected function getDateFormat()
    {
        return 'U';
    }

}

Query Scopes

Scopes 讓我們可以重複使用 model 裡面的 query 邏輯。要定義一個 scope,只要簡單的在 model 方法加上一個前綴"scope"。

定義一個 Query Scope
<?php

class User extends Eloquent {

    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    public function scopeWomen($query)
    {
        return $query->whereGender('W');
    }

}

使用一個 Query Scope
<?php

$users = User::popular()->women()->orderBy('created_at')->get();

動態 Scopes

有時候您會需要定義一個可以接受參數的 scope。只要很單純的增加一個變數到 scope function 即可:

<?php

class User extends Eloquent {

    public function scopeOfType($query, $type)
    {
        return $query->whereType($type);
    }

}

` 使用如下:
<?php

$users = User::ofType('member')->get();

Relationships

當然,您的資料庫表格很可能會和其他的相關。Eloquent 可以讓我們很簡單的去處理這些關連。Laravel 支援許多種類的關連。

- One To One
- One To Many
- Many To Many
- Has Many Through
- Polymorphic Relations
- Many To Many Polymorphic Relations
One to One

舉例來說,一個 User model 會有一個 Phone Model。我們可以在 Eloquent 定義這些關係。

定義一個 1對1關係
<?php

class User extends Eloquent {

    public function phone()
    {
        return $this->hasOne('Phone');
    }

}

傳入 hasOne()方法的第一個參數是關連 model 的名稱。一旦關連被建立,我們便可以使用 Eloquent 的動態屬性取得關連 model。

<?php

$phone = User::find(1)->phone;

實際執行的SQL語句如下:

SELECT * FROM users WHERE id = 1

SELECT * FROM phones WHERE user_id = 1

這邊要留意的是,Eloquent 會假定關連的外鍵是基於 model 的名稱,以這個例子來說,Phone model 會假設 user_id為外鍵。如果您希望覆寫這個轉換規則,您可以傳入第二個參數給 hasOne()方法。除此之外,您也可以傳入第三個參數給這個方法,去指定要使用哪個欄位當做關連。

<?php

return $this->hasOne('Phone', 'foreign_key');

return $this->hasOne('Phone', 'foreign_key', 'local_key');
定義反向關連

使用 belongsTo()方法去定義 Phone model 的反向關連:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User');
    }

}

在上面這個例子中,Eloquent 會根據 phone table 裡的 user_id 欄位當做外鍵。如果您想要自行定義不同的欄位當外鍵,可以在 belongsTo()方法傳入第二個參數:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User', 'local_key');
    }

}

您也可以傳入第三個參數指定父表格關連欄位:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User', 'local_key', 'parent_key');
    }

}

One to Many

我們以一篇文章對應多個評論來舉例:

<?php

class Post extends Eloquent {

    public function comments()
    {
        return $this->hasMany('Comment');
    }

}


現在我們可以透過動態屬性取得 post 的 comment:

<?php

$comments = Post::find(1)->comments;

如果您想要增加限制去過濾取得的 comments,可以使用 comments()方法並且串接上條件:

<?php

$comments = Post::find(1)->comments()->where('title', '=', 'foo')->first();

同樣的,您也可以藉由傳入第二個參數給 hasMany()方法去覆寫轉換外鍵的邏輯。和 hasOne的關聯一樣,欄位可以指定如下:

<?php

return $this->hasMany('Comment', 'foreign_key');

return $this->hasMany('Comment', 'foreign_key', 'local_key');

定義 comment Model 的反向關連,可以使用 belongsTo()方法:

定義反向關連
<?php

class Comment extends Eloquent {

    public function post()
    {
        return $this->belongsTo('Post');
    }

}

Many to Many

多對多關連是最複雜的關聯種類。多個使用者( users )對多個不同權限( roles )是實務上很常見的多對多關係。實現這樣的多對多關係需要三個表格: users, roles, role_user。 role_user 表格是從兩個關連 model 的名稱和照字母順序衍生出來的,該表格應該要有 user_id,role_id 兩個欄位。

我們可以使用 belongsToMany()方法定義多對多關係:

<?php

class User extends Eloquent {

    public function roles()
    {
        return $this->belongsToMany('Role');
    }

}

現在,我們可以從 User Model 中取得 roles:

<?php

$roles = User::find(1)->roles;

當然,您也可以對 belongsToMany()方法傳入第二個參數去指定樞紐表格的名稱:

<?php

return $this->belongsToMany('Role', 'user_roles');

也可以對Role Model 定義反向關係:

<?php

class Role extends Eloquent {

    public function users()
    {
        return $this->belongsToMany('User');
    }

}

Has Many Through

"has many through"關連提供了一個方便的方法幫助我們透過一個直接關連取得隔了一層以上關連的關連model。舉例來說,一個 Country model和許多 User model 關連,而User model 又和 Post model 關連。表格的關係如下圖:

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

如您所見,posts 表格並沒有包含一個 country_id 的欄位,但 hasManyThrough關連可以讓我們藉由 $country->posts 取得屬於某個 country下的posts。

<?php

class Country extends Eloquent {

    public function posts()
    {
        return $this->hasManyThrough('Post', 'User');
    }

}

如果您想要手動替關連指定鍵,可以把第三和第四個參數傳入方法中:

<?php

class Country extends Eloquent {

    public function posts()
    {
        return $this->hasManyThrough('Post', 'User', 'country_id', 'user_id');
    }

}

Polymorphic Relations

多型關係允許一個 model 屬於一個或多個 model 之下。舉例來說,有一個 photo model 既屬於 staff model ,也屬於 order model 之下。我們可以這樣定義他們的關聯:

<?php

class Photo extends Eloquent {

    public function imageable()
    {
        return $this->morphTo();
    }

}

class Staff extends Eloquent {

    public function photos()
    {
        return $this->morphMany('Photo', 'imageable');
    }

}

class Order extends Eloquent {

    public function photos()
    {
        return $this->morphMany('Photo', 'imageable');
    }

}

現在,不論從 staff 或是 member 都可以取得 photo 了。

取得一個多型關連
<?php

$staff = Staff::find(1);

foreach ($staff->photos as $photo)
{
    //
}

取得某個多型關連的所有者

然而,真正的"多型"魔術發生在從photo model 取得 staff 或是 order 時:

<?php

$photo = Photo::find(1);

$imageable = $photo->imageable;

Photo model 上的可視( imageable )關連會回傳 Staff 或是 Order 實體,這取決於擁有該 Photo 的是哪種 model。
為了幫助釐清,讓我們來看看多型關連在資料庫的結構:

staff
    id - integer
    name - string

orders
    id - integer
    price - integer

photos
    id - integer
    path - string
    imageable_id - integer
    imageable_type - string

特別注意 表格Photo 上的 imageable_id 以及 imageable_type 兩個欄位。id 即為對應的 id 值,而type是對應 model 的類別名稱。在取得可視關連時,這讓 ORM 能夠判斷應該回傳哪種 model。

多對多多型關連

除了傳統的多型關連之外,您也可以指定多對多的多型關連。舉例來說,部落格的 Post 和 Video模組分享一個到 Tag model 的多型關連。首先,來看看資料庫結構:

多型多對多關連表格結構
posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

接著,我們準備來設置 model 的關聯。Post 和 Video 模組都有一個透過 tags()方法的 morphToMany 關連。

<?php

class Post extends Eloquent {

    public function tags()
    {
        return $this->morphToMany('Tag', 'taggable');
    }

}

tag model 可以對每個關連定義分別方法:

<?php

class Tag extends Eloquent {

    public function posts()
    {
        return $this->morphedByMany('Post', 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany('Video', 'taggable');
    }

}

Querying Relations

當取得某個 model 的紀錄時,您可能會希望根據某個關連來限制取得的結果。舉例來說,您希望取得所有至少有一則評論的 Post。要做這點,您可以使用 has()方法:

Select資料時詢問關連
<?php

$posts = Post::has('comments')->get();

您也可以指定一個比較子和計數

<?php

$posts = Post::has('comments', '>=', 3)->get();

如果還需要更強大的功能,可以使用 whereHas 以及 orWhereHas 兩個方法,將 where 條件結合 has:

<?php

$posts = Post::whereHas('comments', function($q)
{
    $q->where('content', 'like', 'foo%');

})->get();

動態屬性( Dynamic Properties )

Eloquent允許您透過動態屬性取得關連。Eloquent 會自動的為您讀取關連,甚至聰明到可以判斷應該使用 get( 1對多關連時使用 ) 或是 first( 1對1關連時使用 )。他們將可以在動態屬性以和關連模組相同的名稱被取得。例如:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User');
    }

}

$phone = Phone::find(1);

原本這樣的呼叫:

<?php

echo $phone->user()->first()->email;

可以改成

<?php

echo $phone->user->email;

注意: 如果關連回傳的實體為兩個以上的話,將會回傳一個 Illuminate\Database\Eloquent\Collection 的實體。

本篇回顧

這篇內容真的好多啊,整整花了我一天,比預期還要多很多的時間( 怎們跟公司交代啊Orz )。熬夜工作無妨,熬夜看書就真的毫無效率可言,明天再繼續 Eloquent ORM 的下半部吧。假日看來也要繼續趕工了。

參考資料

Laravel-Eloqunet ORM :http://laravel.com/docs/eloquent

Laravel-Query Builder

Introduction

Query Builder( Query產生器,後面還是用原文表示 )提供一個方便流暢的介面去建立或是執行queries。這可以被用來執行應用程式大部分的資料庫操作,而且可以在所有支援的資料庫系統皆能運作。

注意: Laravel的 query builder 使用 PDO 變數綁定來保護您的應用程式防止 SQL Injection,因此您不需要再去花費功夫過濾被綁定的字串。

Selects

從表單取得所有的列
<?php

$users = DB::table('users')->get();

foreach ($users as $user)
{
    var_dump($user->name);
}
從表單取得一個列
<?php

$user = DB::table('users')->where('name', 'John')->first();

var_dump($user->name);
從一列中取得一欄
<?php

$name = DB::table('users')->where('name', 'John')->pluck('name');

從表單中取得某欄的值
<?php

$roles = DB::table('roles')->lists('title');

此方法會回傳一個 title 的陣列,您也可以給回傳的陣列設定一個鍵值。

<?php

$roles = DB::table('roles')->lists('title', 'name');

指定一個 Select 子句
<?php

$users = DB::table('users')->select('name', 'email')->get();

$users = DB::table('users')->distinct()->get();

$users = DB::table('users')->select('name as user_name')->get();
新增一個Select子句至以存在的 Query
<?php

$query = DB::table('users')->select('name');

$users = $query->addSelect('age')->get();
使用Where操作子
<?php

$users = DB::table('users')->where('votes', '>', 100)->get();
使用Or語句
<?php

$users = DB::table('users')
  ->where('votes', '>', 100)
  ->orWhere('name', 'John')
  ->get()
;
使用 Where between
<?php

$users = DB::table('users')
  ->whereBetween('votes', array(1, 100))
  ->get()
;
使用 Where not between
<?php

$users = DB::table('users')
  ->whereNotBetween('votes', array(1, 100))
  ->get()
;
使用Where IN
<?php

$users = DB::table('users')
 ->whereIn('id', array(1, 2, 3))
 ->get()
;

$users = DB::table('users')
 ->whereNotIn('id', array(1, 2, 3))
 ->get()
;
使用 whereNull 找尋沒有給值的記錄
<?php

$users = DB::table('users')
 ->whereNull('updated_at')
 ->get()
;
Order By, Group By, Having
<?php

$users = DB::table('users')
  ->orderBy('name', 'desc')
  ->groupBy('count')
  ->having('count', '>', 100)
  ->get()
;
Offset & Limit
<?php

$users = DB::table('users')->skip(10)->take(5)->get();

Joins

Query Builder 可以使用 join 語句,如下:

最基本的 JOIN 語句:
<?php

DB::table('users')
  ->join('contacts', 'users.id', '=', 'contacts.user_id')
  ->join('orders', 'users.id', '=', 'orders.user_id')
  ->select('users.id', 'contacts.phone', 'orders.price')
;

Left Join 語句:
<?php

DB::table('users')
 ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
 ->get()
;

您也可以指定更多進階的 join 子句:

<?php

DB::table('users')
  ->join('contacts', function($join)
  {
    $join->on('users.id', '=', 'contacts.user_id')->orOn(...);
  })
  ->get()
;
如果您想要用 where 形式的子句在您的join上,您可以使用 where 和 onWhere 方法。
<?php

DB::table('users')
  ->join('contacts', function($join)
  {
    $join->on('users.id', '=', 'contacts.user_id')->where('contacts.user_id', '>', 5);
  })
  ->get()
;

Advanced Wheres

有時候您可能會需要新增更多的進階where子句例如 "where exists",或是嵌套一組變數群。Laravel 的 Query Builder 也能夠處理這些:

Parameter Grouping( 把變數聚集 )
<?php

DB::table('users')
  ->where('name', '=', 'John')
  ->orWhere(function($query)
  {
    $query->where('votes', '>', 100)->where('title', '<>', 'Admin');
  })
  ->get()
;
上面的 query 會產生入下 SQL
SELECT * FROM users WHERE name = 'John' OR (votes > 100 AND title <> 'Admin')
Exists 語句
<?php

DB::table('users')
  ->whereExists(function($query)
  {
    $query
      ->select(DB::raw(1))
      ->from('orders')
      ->whereRaw('orders.user_id = users.id')
    ;
  })
  ->get()
;

select * from users
where exists (
    select 1 from orders where orders.user_id = users.id
)

Aggregates

Query Builder 也提供了許多統計的方法,例如 count、max、min、avg、以及 sum。

使用統計方法
<?php

$users = DB::table('users')->count();

$price = DB::table('orders')->max('price');

$price = DB::table('orders')->min('price');

$price = DB::table('orders')->avg('price');

$total = DB::table('users')->sum('votes');

Raw Expressions

有時候您需要在query中使用原生的表達式。這些表達式會以字串的形態被注入到 query中,所以使用時要小心 SQL injection 的問題。您可以使用 DB::raw() 方法來新增一個原生的表達式。

使用原生表達式
<?php

$users = DB::table('users')
  ->select(DB::raw('count(*) as user_count, status'))
  ->where('status', '<>', 1)
  ->groupBy('status')
  ->get()
;
增加或減少某個欄位的數值
<?php

DB::table('users')->increment('votes');

DB::table('users')->increment('votes', 5);

DB::table('users')->decrement('votes');

DB::table('users')->decrement('votes', 5);
您也可以額外指定需要改變的欄位
<?php

DB::table('users')->increment('votes', 1, array('name' => 'John'));

Inserts

新增一筆記錄到表格中
<?php

DB::table('users')->insert(
    array('email' => 'john@example.com', 'votes' => 0)
);

如果表格有一個自動累加的 id,使用 insertGetId()方法新增一筆記錄並且取得 id。

<?php

$id = DB::table('users')->insertGetId(
    array('email' => 'john@example.com', 'votes' => 0)
);

注意: 當使用PostgreSQL時,使用 insertGetId() 方法取得的自動累加欄位的欄位名稱必須為 "id"。

新增多筆記錄到表格中
<?php

DB::table('users')->insert(array(
    array('email' => 'taylor@example.com', 'votes' => 0),
    array('email' => 'dayle@example.com', 'votes' => 0),
));

Updates

更新table某筆記錄
<?php

DB::table('users')
  ->where('id', 1)
  ->update(array('votes' => 1))
;

Deletes

刪除table中某個範圍的記錄
<?php

DB::table('users')->where('votes', '<', 100)->delete();
刪除table中的所有紀錄
<?php

DB::table('users')->delete();
truncate(其實也是刪除所有記錄,只是 autoincrement 會重設) 一個table
<?php

DB::table('users')->truncate();

Unions

Query Builder也提供了一個快速將兩個 queries 聯集的方法:

執行Query的聯集
<?php

$first = DB::table('users')->whereNull('first_name');

$users = DB::table('users')->whereNull('last_name')->union($first)->get();

union() 也可以寫作 unionAll(),效果是一樣的。

Pessimistic Locking

以 "分享鎖" 的方式執行 Select 語句,您可以使用 sharedLock() 方法:

<?php

DB::table('users')->where('votes', '>', 100)->sharedLock()->get();

將 Select語句加上 "更新鎖",可以使用 lockForUpdate() 方法:

<?php

DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

Caching Queries

您可以使用 remember()方法很簡單的將 query 的結果做緩存:

緩存一個Query的結果
<?php

$users = DB::table('users')->remember(10)->get();

在這個例子中,query 的結果會被緩存10分鐘。當結果被緩存後,query 不會再次訪問 database,而是直接從預設的快取驅動器取得快取。

如果您的快取驅動器有支援 tag 功能,可以使用如下:( 預設的 file 機制不支援tag )

<?php

$users = DB::table('users')->cacheTags(array('people', 'authors'))->remember(10)->get();

本篇回顧

和 Doctrine 的 Query Builder比起來,Laravel 真的簡單多了,或許 Doctrine 比較強大,但是我們不見得需要使用到如此多的功能。一般網站需要的 CRUD 功能,Laravel 的 Query Builder 看起來其實已經綽綽有餘了。接下來是 Eloquent ORM 的介紹囉,這應該是重頭戲中的重頭戲了,真期待!

參考資料

Laravel Database Eloquent ORM: http://laravel.com/docs/queries

Puree網站的1大六小廣告區實作

動機

這大概是四個月前寫的東西了。當時有客戶覺得 PUREE 首頁左邊那個活動廣告很不錯,他也想要網站有這個功能。稍微看了一下覺得並不難做,便在半好玩的心態下將它完成了。不過後來客戶又說他不需要了,結果這個小作品就一直被擱置在角落最後就被忘掉了。剛剛在整理檔案時又發現了它,現在重新看看覺得還蠻好玩的,勉強算是個可以用的東西,就在這邊和大家分享一下囉。

分析

該套件大概粗分三大部分功能:

  1. 上面比較大的那塊是個簡單的水平方向的圖片輪播
  2. 下方六小格在滑鼠移動上去時,上方區域的圖片會移動到對應的編號
  3. 有個移動的框框會移動到滑鼠最後滑上去的六小格上

步驟

1. 靜態的 html 以及 css
puree.html
<!doctype html>
<html lang="zh-tw">
<head>
    <meta charset="UTF-8">
    <title>六方格+換圖片動畫</title>
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/puree.css" />
</head>
<body>
<!-- 
主要達成功能:
=======================================================================
當滑鼠移動到下方的小圖片( 其實是事件是綁在div )上時,大塊的地方會動態切換到對應標號的圖片( 其實是換div ),例子裡只有文字,
大家要用的話自行將文字改為圖片即可。
-->
<div class="puree_six_ad" >
    <div class="top">
        <div class="gallery">
            <div>1.</div>
            <div>2.</div>
            <div>3.</div>
            <div>4.</div>
            <div>5.</div>
            <div>6.</div>
        </div>
    </div>
    <div class="bottom">
        <div>1.</div>
        <div>2.</div>
        <div>3.</div>
        <div>4.</div>
        <div>5.</div>
        <div>6.</div>
    </div>
</div>
</body>
</html>
css/puree.css
div.puree_six_ad {
  position: absolute;
  width: 260px;
  height: 550px;
  margin: 20px 0 20px 200px;
  border: red solid 2px;
  overflow: hidden; }
  div.puree_six_ad div.top {
    width: 260px;
    height: 300px;
    border: yellow solid 1px; }
    div.puree_six_ad div.top div.gallery {
      position: absolute;
      width: 1560px;
      height: 300px; }
      div.puree_six_ad div.top div.gallery div {
        float: left;
        width: 260px;
        height: 300px; }
div.bottom div {
  width: 80px;
  height: 104px;
  float: left;
  border-right: #dedede 1px solid;
  margin: 5px 0 3px 5px; }
  div.bottom div:nth-of-type(3), div.bottom div:last-of-type {
    border-style: none; }
div.float_frame {
  position: absolute;
  border: black 2px solid; }

完成後應該可以看到這樣的畫面:

2. 實作功能分析中的第一項功能-水平輪播
引入javascript
puree.html
<script src="js/jquery-1.9.1.js"></script>
<script src="js/puree.js"></script>

撰寫 javascript
js/puree.js
jQuery(function ($) {

    var view = {};// 宣告視圖物件


    /* 將需要用到的 DOM 元素存進 view 裡頭 */
    view.$all = $('.puree_six_ad'); // 整個廣告區塊

    view.$top = view.$all.find('.top'); // 圖片輪播區域

    view.$gallery = view.$top.find('.gallery'); // 圖片輪播區域中的圖片區塊

    view.$eachPic = view.$gallery.children(); // 各張輪播圖片

    view.$bottom = view.$all.find('.bottom'); // 底部區塊,包含六個 div( 即六小圖區域 )

    view.$sixSmall = view.$bottom.children(); // 六小圖


    /* 先將之後動畫必定會用到的一些數值求出 */
    view.nTopWidth = parseInt( view.$top.width() ); // 輪播圖片外框寬度

    view.nGalleryWith = parseInt( view.$gallery.width() ); // 輪播圖片寬度

    view.nShift = (view.nGalleryWith - view.nTopWidth); // 寬度差,用來判斷是否已經輪播到最後一張圖片

  
  /* 浮動框框會用到,先在這邊求出 */
  view.nSmallMarginLeft = parseInt(view.$sixSmall.eq(0).css('marginLeft')); // 六小圖 marginTop 的值

    view.nSmallMarginTop = parseInt(view.$sixSmall.eq(0).css('marginTop')); // 六小圖 marginLeft 的值


    var config = {}; // 宣告參數物件

    config.nSpeed = 500;
    config.nTimeInterval = 2000;

    // 利用改變 css 的 'left' 屬性數值實現圖片換張的效果

    var autoGalleryPlay = function () {
        view.autoRun = setTimeout(function () {

            // 取得目前 $gallery 的 css 'left' 屬性值 

            var nNowLeft = parseInt( view.$gallery.css( 'left' ), 10 );
            
            // 切換圖片動畫

            view.$gallery.stop().animate({
                // 計算切換到下一張圖片時應該改變的 left 值

                left: getNextLeft( isNaN( nNowLeft ) ? 0 : nNowLeft )
            }, config.nSpeed, function () {
                autoGalleryPlay(); // 遞迴執行播放動作

            });      
        }, config.nTimeInterval );
    };

    /**
     * 取得下一次輪播需要的 left 屬性數值 
     * @param  {[number]} nNowLeft [目前內框的 left 值]
     * @return {[number]}          
     */
    var getNextLeft = function ( nNowLeft ) {
        // 如果左移量已經比內外區塊寬度差還大,表示為最後一張圖片:

        // true: 表示為最後一張圖片,回傳0回到第一張

        // false: 則左移一個父框寬度,切換到下一張

        return ((view.nShift + nNowLeft) <= 0) ? 0 : (nNowLeft - view.nTopWidth);         
    };

    // 啟動輪播

    autoGalleryPlay();
});  

測試後確定輪播效果正常呈現,再來就是讓我們加上滑鼠移到整個廣告區塊時,暫停輪播的事件:
請將 "// 啟動輪播" 以後的程式碼改成這樣:

js/puree.js
//...


// 綁定啟動輪播事件以及取消輪播事件 

view.$all.mouseover(function () {
    return clearTimeout( view.autoRun );
}).mouseleave(function () {
    return autoGalleryPlay();
}).mouseleave();// DOM讀取完成後自動 onFire 觸發輪播動作


重整後試試看把您的滑鼠移到廣告區塊時,動畫有沒有停止呢?
如果有,那恭喜您,輪播功能差不多 ok 拉!接下來實作六小圖控制上方輪播圖片的功能吧 ~
( 如果沒有,或是圖片根本一開始就不會輪播了,請打開主控台檢查有無錯誤 )

3. 實作分析功能中的第二項功能-選擇顯示輪播圖片

請在您的 puree.js 最下方追加以下程式碼:

js/puree.js
// ...


    /*
        滑鼠移到六小圖上後,輪播圖片切換到對應的圖片      
        1. 先抓出小圖在兄弟元素中的索引值( 第幾個 )
        2. 該索引值乘以父框寬度即輪播圖片應變換的 'left' 值,
        更清楚講就是,假個該小圖是第 n 張圖,則上方輪播區域就需要移動 (n - 1) 個父元素寬度 
     */ 
    view.$sixSmall.mouseover(function () {
        var nIndex = $(this).index();

      view.$gallery.stop().animate({
        left: -(nIndex * view.nTopWidth) 
      }, config.nSpeed );  
    }).on('mouseover.anifloatFrame', function () {
        view.$floatFrame.stop().animate({
            'top': $(this).offset().top - view.nSmallMarginTop,
            'left': $(this).offset().left - view.nSmallMarginLeft
        }, config.nMouseSpeed );       
    });

現在把滑鼠移到下方得六小圖,上面的輪播區域也會跟著移動囉。
測試看看吧,如果沒有問題就前往最後一步吧。

4. 實作分析功能中的第三項功能-浮動框框

這邊的作法大概是這樣的:

1.用 jQuery 加上一個 div元素,就是我們的框框
js/puree.js
//...


view.$floatFrame = $(document.createElement('div')); // 產生浮動框框div


/*
1. 加上 class = float_frame
2. 將它放入body
3. 擺在第一個六小圖上,並且配置相關 css 屬性值
*/
view.$floatFrame.addClass('float_frame').appendTo('body').css({
    top: view.$sixSmall.eq(0).offset().top - view.nSmallMarginTop,
    left: view.$sixSmall.eq(0).offset().left - view.nSmallMarginLeft,
    width: view.$sixSmall.outerWidth(),
    height: view.$sixSmall.outerHeight(true)
});

2.綁定滑鼠移動到六小圖上時,框框會跟著移動的動畫

請將 view.$sixSmall 的綁定事件改成如下程式碼

  /*
        switchGallery:
        滑鼠移到六小圖上後,輪播圖片切換到對應的圖片      
        1. 先抓出小圖在兄弟元素中的索引值( 第幾個 )
        2. 該索引值乘以父框寬度即輪播圖片應變換的 'left' 值,
        更清楚講就是,假個該小圖是第 n 張圖,則上方輪播區域就需要移動 (n - 1) 個父元素寬度 

        aniFloatFrame:
        讓浮動框框移動到現在滑鼠上方的六小圖
     */ 
    view.$sixSmall.on('mouseover.switchGallery', function () {
        var nIndex = $(this).index();

      view.$gallery.stop().animate({
        left: -(nIndex * view.nTopWidth) 
      }, config.nSpeed );  
    }).on('mouseover.anifloatFrame', function () {
        view.$floatFrame.stop().animate({
            'top': $(this).offset().top - view.nSmallMarginTop,
            'left': $(this).offset().left - view.nSmallMarginLeft
        }, config.nMouseSpeed );       
    });

浮動框框會跑來跑去囉~~

3.浮動框框造成的小錯誤修正

由於框框是加上去的,所以會蓋在六小圖上,這會導致取消輪播事件無法觸發,所以取消的事件觸發元素要把框框加上去。
找到取消輪播事件的程式碼,改成如下:

    // 綁定啟動輪播事件以及取消輪播事件 

    view.$all.add(view.$floatFrame).mouseover(function () {
        return clearTimeout( view.autoRun );
    }).mouseleave(function () {
        return autoGalleryPlay();
    }).mouseleave();

OK!該有的功能都齊備了!
不過我們可以做得更好,現在把程式碼加進 jQuery方法變成誰都會用的小套件吧!
將檔案改成 jquery.sixad_crausel.js,並且整理得更好閱讀一點。請將程式碼改成如下:

js/jquery.sixad_crausel.js
;(function ($) {

  $.fn.sixadCrausel = function () {
    var SixAD = function ($e) {
      this.view = {};// 宣告視圖物件

      this.config = {};// 宣告參數物件

      return this.init($e);
    };

    /**
     * 將需要用到的 DOM 元素存進 view 裡頭
     * @param  [jQuery Obj]
     * @return this 
     */
    SixAD.prototype.setView = function ($e) {
      var view = this.view;
      view.$all = $e; // 整個廣告區塊

      view.$top = view.$all.find('.top'); // 圖片輪播區域

      view.$gallery = view.$top.find('.gallery'); // 圖片輪播區域中的圖片區塊

      view.$eachPic = view.$gallery.children(); // 各張輪播圖片

      view.$bottom = view.$all.find('.bottom'); // 底部區塊,包含六個 div( 即六小圖區域 )

      view.$sixSmall = view.$bottom.children(); // 六小圖


      /* 先將之後動畫必定會用到的一些數值求出 */
      view.nTopWidth = parseInt( view.$top.width() ); // 輪播圖片外框寬度

      view.nGalleryWith = parseInt( view.$gallery.width() ); // 輪播圖片寬度

      view.nShift = (view.nGalleryWith - view.nTopWidth); // 寬度差,用來判斷是否已經輪播到最後一張圖片


      /* 浮動框框會用到,先在這邊求出 */
      view.nSmallMarginLeft = parseInt(view.$sixSmall.eq(0).css('marginLeft')); // 六小圖 marginTop 的值

      view.nSmallMarginTop = parseInt(view.$sixSmall.eq(0).css('marginTop')); // 六小圖 marginLeft 的值

      return this;
    };

    /**
     * 設定動畫相關參數
     */
    SixAD.prototype.setConfig = function () {
      this.config.nSpeed = 500;
      this.config.nTimeInterval = 2000;
      return this;
    };

    // 利用改變 css 的 'left' 屬性數值實現圖片換張的效果

    SixAD.prototype.autoGalleryPlay = function () {
      var self = this,
          view = self.view;
      view.autoRun = setTimeout(function () {
        // 取得目前 $gallery 的 css 'left' 屬性值 

        var nLeft = parseInt( view.$gallery.css( 'left' ), 10 );
        // 切換圖片動畫

        self.switchImgAnimate( isNaN( nLeft ) ? 0 : nLeft );
      }, self.config.nTimeInterval );
    };

    /**
     * 切換圖片動畫
     *
     * @param  [number] nLeft
     * @return this
     */
    SixAD.prototype.switchImgAnimate = function ( nLeft ) {
      var self = this,
          view = self.view;
      // 切換圖片動畫

      view.$gallery.stop().animate({
        // 計算切換到下一張圖片時應該改變的 left 值

        left: self.getNextLeft( nLeft )
      }, self.config.nSpeed, function () {
        self.autoGalleryPlay(); // 遞迴執行播放動作

      });   
    };

    /**
     * 取得下一次輪播需要的 left 屬性數值 
     * @param  {[number]} nNowLeft [目前內框的 left 值]
     * @return {[number]}          
     */
    SixAD.prototype.getNextLeft = function ( nNowLeft ) {
      var view = this.view;
      // 如果左移量已經比內外區塊寬度差還大,表示為最後一張圖片:

      // true: 表示為最後一張圖片,回傳0回到第一張

      // false: 則左移一個父框寬度,切換到下一張

      return ((view.nShift + nNowLeft) <= 0) ? 0 : (nNowLeft - view.nTopWidth);     
    };

    /**
     * 綁定啟動輪播事件以及取消輪播事件
     * @return this
     */
    SixAD.prototype.bindTimerControll = function () {
      var self = this,
          view = self.view;
      view.$all.add(view.$floatFrame).mouseover(function () {
        return clearTimeout( view.autoRun );
      }).mouseleave(function () {
        return self.autoGalleryPlay();
      }).mouseleave();
      return this;
    };

    /**
     * 產生浮動框框並且放置於指定位置
     * @return this
     */
    SixAD.prototype.genFloatFrame = function () {
      var view = this.view;
      view.$floatFrame = $(document.createElement('div')); // 產生浮動框框div

    
      /*
        1. 加上 class = float_frame
        2. 將它放入body
        3. 擺在第一個六小圖上,並且配置相關 css 屬性值
       */
      view.$floatFrame.addClass('float_frame').appendTo('body').css({
        top: view.$sixSmall.eq(0).offset().top - view.nSmallMarginTop,
        left: view.$sixSmall.eq(0).offset().left - view.nSmallMarginLeft,
        width: view.$sixSmall.outerWidth(),
        height: view.$sixSmall.outerHeight(true)
      });
      return this;
    };

    /*
      switchGallery:
      滑鼠移到六小圖上後,輪播圖片切換到對應的圖片    
      1. 先抓出小圖在兄弟元素中的索引值( 第幾個 )
      2. 該索引值乘以父框寬度即輪播圖片應變換的 'left' 值,
      更清楚講就是,假個該小圖是第 n 張圖,則上方輪播區域就需要移動 (n - 1) 個父元素寬度 

      aniFloatFrame:
      讓浮動框框移動到現在滑鼠上方的六小圖
     */ 
    SixAD.prototype.bindSixSmallEvent = function () {
      var self = this,
          view = this.view;
      view.$sixSmall.on('mouseover.switchGallery', function () {
        var nIndex = $(this).index();

        view.$gallery.stop().animate({
          left: -(nIndex * view.nTopWidth) 
        }, self.config.nSpeed );  
      }).on('mouseover.anifloatFrame', function () {
        view.$floatFrame.stop().animate({
          'top': $(this).offset().top - view.nSmallMarginTop,
          'left': $(this).offset().left - view.nSmallMarginLeft
        }, self.config.nMouseSpeed );   
      });
      return this;
    };

    // 初始化廣告區域程式

    SixAD.prototype.init = function ($e) {
      this.setView($e).setConfig().genFloatFrame().bindSixSmallEvent().bindTimerControll();
      return $e;
    };

    return new SixAD(this);
  };

})(jQuery); 

puree.html
<!doctype html>
<html lang="zh-tw">
<head>
    <meta charset="UTF-8">
    <title>六方格+換圖片動畫</title>
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/puree.css" />
    <script src="js/jquery-1.9.1.js"></script>
    <script src="js/jquery.sixad_crausel.js"></script>
</head>
<body>
<!-- 
主要達成功能:
=======================================================================
當滑鼠移動到下方的小圖片( 其實是事件是綁在div )上時,
大塊的地方會動態切換到對應標號的圖片( 其實是換div ),例子裡只有文字,
大家要用的話自行將文字改為圖片即可。
-->
<div class="puree_six_ad">
    <div class="top">
        <div class="gallery">
            <div>1.</div>
            <div>2.</div>
            <div>3.</div>
            <div>4.</div>
            <div>5.</div>
            <div>6.</div>
        </div>
    </div>
    <div class="bottom">
        <div>1.</div>
        <div>2.</div>
        <div>3.</div>
        <div>4.</div>
        <div>5.</div>
        <div>6.</div>
    </div>
</div>
<script>
jQuery(function ($) {
    $('.puree_six_ad').sixadCrausel();
});
</script>
</body>
</html>

DEMO連結

無聊的一鍵開啟四個剛剛好的瀏覽器新視窗

動機:

看到 Ricky 大在 PHP conf 表演 socket 的東西時,用了一個按下去會同時開啟四個新視窗並且剛好在銀幕的四個區塊,覺得還蠻有趣的,於是乎想來試試之~。

目標:

一鍵開啟四個將畫面等分的視窗。

步驟:

先找看看有哪些可用的屬性 ( 找到這個 ):

-> 已經可以控制新視窗的大小( width, height)和位置( left, top )了

我們還需要知道如何找出銀幕的寬和高:

-> screen.height , screen.width

該有的都有了,可以動手了!

程式碼:

<!doctype html>
<html lang="zh-tw">
<head>
  <meta charset="UTF-8">
  <title>一次開四個</title>
  <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
</head>
<body>
  <a href="#">開四個新視窗</a>
 
<script>
$(function () {
  openByCustomConfig = function (oConfig) {
    var 
      _url = oConfig.url,
      _windowName = oConfig.windowName,
      _left = oConfig.left,
      _top = oConfig.top,
      _config = 'toolbar=no,location=no,status=no, menubar=no';
 
    _config+= ',width=' + screen.width * 0.49;
    _config+= ',height=' + screen.height * 0.42;
    _config+= ',left=' + _left;
    _config+= ',top=' + _top;
    return window.open( _url, _windowName, _config);
  };
 
  $('a').click(function () {
    var 
      _leftDis = screen.width * 0.50,
      _topDis = screen.height * 0.50,
      oConfig = {url: 'http://tw.yahoo.com'};
 
    oOptLT = $.extend( {}, oConfig, {windowName: 'Yahoo1', left: 0, top: 0});
    oOptRT = $.extend( {}, oConfig,  {windowName: 'Yahoo2', left: _leftDis, top: 0});
    oOptLB = $.extend( {}, oConfig, {windowName: 'Yahoo3', left: 0, top: _topDis});
    oOptRB = $.extend( {}, oConfig, {windowName: 'Yahoo4', left: _leftDis, top: _topDis});
 
    openByCustomConfig( oOptLT );
    openByCustomConfig( oOptRT );
    openByCustomConfig( oOptLB );
    openByCustomConfig( oOptRB );
  });
});
 
</script>
</body>
</html>

後記:

雖然看起來有點無聊,不過要做某些測試或介紹的時候這個東西應該還微有用。

Composer autoload

使用方法

1.在composer.json加上這段:
composer.json
"autoload": {  
    "psr-0": {  
      "": "src"  
    }  
  }
2.執行composer dump-autoload

預期效果:

src 的資料夾下的 class ,只要和檔名相同在調用時統統都會自動 autoload

實際實驗:

src/helloJocoonopa.php
<?php  
class helloJocoonopa  
{  
   public function sayHello ()  
   {  
      echo __CLASS__ . '!!';  
   }  
}  
index.php
<?php
require_once( 'vendor/autoload.php'); // 這是 composer 產生的 autoload 檔案  
  
$class = array(new helloJocoonopa, 'sayHello');  
$class(); // helloJocoonopa !!  

心得

不太清楚 PSR-0 和 PSR-4的效果差別,嘗試加深目錄結構和命名空間,就完全讀不到檔案 ~ 這部份可能還是要請教一下。

但我們可以想像,配合 Symfony2Routing component,可以輕鬆不費力的建造一個簡單的 Controller-Router 架構。

後記:

除了PSR-0 外,還有一個 PSR-4 的標準,詳細介紹 。

PSR-4和PSR-0最大的區別是對下劃線(underscore)的定義不同。PSR-4中,在類名中使用下劃線沒有任何特殊含義。而PSR-0則規定類名中的下劃線
_會被轉化成目錄分隔符。

舉例: 以 jocoonopa_hello這個命名空間來說,兩個標準分別對應的檔案路徑為

PSR-0: jocoonopa/hello
PSR-4: jocoonopa_hello

不過照理來說現在的 Class 或命名空間都不會用底線了,所以 PSR-0 和 PSR-4其實基本無差別。